chore(docker): stamp the API's logs in Asia/Jakarta

The browser sidecar needs a non-UTC zone to clear Cloudflare's challenge,
which left the two services logging on different clocks - the API in UTC,
the sidecar in local time. Correlating a poll failure against anything
else then means doing arithmetic at the worst possible moment.

This is cosmetic and nothing else in the service has a zone. Bookmark
timestamps are unix milliseconds (0001_bookmarks.sql pins that: the
userscripts send Date.now() and the ordering rule compares the integers
directly), the only two real time columns are timestamptz, the poller
works in durations, and nothing anywhere formats a wall clock - there is
no .Format( call in non-test backend code. So the zone can only reach
Go's log package, which stamps lines in local time.

No code change and no image change: distroless/static already carries the
full tzdata (1247 entries, Asia/Jakarta among them), so Go resolves the
name straight out of /usr/share/zoneinfo.

Named API_TZ rather than TZ because compose also reads the invoking
shell's environment, and a bare TZ would let an operator's exported zone
silently become the container's.

Verified against the built image, same instant:

  no TZ            2026/08/08 16:24:30 open store: ...
  TZ=Asia/Jakarta  2026/08/08 23:24:30 open store: ...

matching `date -u` 16:24:33 and Jakarta 23:24:33.
This commit is contained in:
2026-08-08 23:25:25 +07:00
parent 1552dd15da
commit a24fcf0f26
2 changed files with 16 additions and 2 deletions
+9 -2
View File
@@ -100,6 +100,13 @@ DISCORD_REDIRECT_URI=
# #
# Unset falls back to the host's /etc/timezone, which is a real zone whenever # Unset falls back to the host's /etc/timezone, which is a real zone whenever
# the host clock is set to local time. Set this when the host runs UTC — a UTC # the host clock is set to local time. Set this when the host runs UTC — a UTC
# server is exactly the case that fails. Only the browser sidecar reads it; # server is exactly the case that fails. Only the browser sidecar reads it —
# the backend keeps its UTC clock. # the backend's own zone is API_TZ below, and is cosmetic.
# BROWSER_TZ=Asia/Jakarta # BROWSER_TZ=Asia/Jakarta
# Zone the backend stamps its log lines in. Cosmetic only — it exists so the
# API's logs read on the same clock as the browser sidecar's. Nothing else in
# the service has a zone: bookmark timestamps are unix ms, and the two real
# time columns are timestamptz. Defaults to Asia/Jakarta; set to UTC for the
# conventional server default.
# API_TZ=Asia/Jakarta
+7
View File
@@ -24,6 +24,13 @@ services:
# comes from .env so it is never committed. # comes from .env so it is never committed.
DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable} DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable}
PORT: "8080" PORT: "8080"
# Log timestamps only. Go's `log` stamps lines in local time, and this
# service has no other use for a zone: bookmark timestamps are unix ms
# and the two real time columns are timestamptz, both absolute instants.
# Purely so these lines read on the same clock as the sidecar's. Named
# API_TZ rather than TZ so an operator's exported shell TZ cannot leak
# in; distroless already carries tzdata, so the name just resolves.
TZ: ${API_TZ:-Asia/Jakarta}
# Discord OAuth for the browser UI (ADR-0002). The first four are # Discord OAuth for the browser UI (ADR-0002). The first four are
# required; DISCORD_REQUIRED_ROLE is optional and empty by default. # required; DISCORD_REQUIRED_ROLE is optional and empty by default.
# Guild membership is the whole gate: any member becomes a Reader. # Guild membership is the whole gate: any member becomes a Reader.