Skip to content

fix(infra): make docker compose actually start and persist data - #39

Merged
ryakel merged 1 commit into
developfrom
claude/its-a-plan-setup-tmojxo
Aug 26, 2026
Merged

fix(infra): make docker compose actually start and persist data#39
ryakel merged 1 commit into
developfrom
claude/its-a-plan-setup-tmojxo

Conversation

@ryakel

@ryakel ryakel commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Tickets: FATH-7, FATH-8

Two defects that together make the documented setup path — cp .env.example .env && docker compose up -d --buildfail on a clean machine. Found by actually running it.

FATH-7: db cannot start (postgres:18 mount)

postgres:18 moved PGDATA from /var/lib/postgresql/data to a major-version-specific path — /var/lib/postgresql/18/docker (docker-library/postgres#1259). All three compose files still mount the pre-18 path and never set PGDATA, so the entrypoint refuses to boot:

Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" ...
       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data (unused mount/volume)

db loops on Restarting (1), and api never starts with dependency db failed to start.

Confirmed this is not a stale-data or migration case: the volume it complains about was empty (4.0K, no PG_VERSION at any depth), created by the failing run itself. The check fires on the existence of the legacy mount, so it reproduces on any clean machine.

The quieter half. Had the entrypoint not errored, the result would be worse: PGDATA resolves inside the container layer while the named volume sits unused, so the stack looks healthy and silently discards the database on every docker compose down. The loud failure is the good case.

Why PGDATA is pinned rather than re-rooting the mount to the upstream-recommended /var/lib/postgresql: anyone holding a volume written by an older image build has their data at the volume root, and moving the mount would strand those files one level up while initdb quietly created a fresh cluster alongside. Pinning works for an empty volume and a populated legacy one alike, and forces a migration on nobody. Trade-off accepted: this diverges from the 18+ layout and makes a future pg_upgrade --link more awkward — the right side of the trade for a single-user self-hosted app.

FATH-8: .env credentials silently ignored

The api service hardcoded DATABASE_URL in its environment: block, which takes precedence over env_file:. So the value in .env was loaded and discarded — while db did honour POSTGRES_PASSWORD from the same file.

Net effect: following .env.example's own security instruction to change the password broke authentication. Copying .env.example verbatim failed too, since CHANGE_ME_BEFORE_PRODUCTIONtruehour. The only working .env was one that ignored the security advice.

Fixed by making the hardcode a ${DATABASE_URL:-...} default. docker-compose.dev.yml is included because as an override it would otherwise re-hardcode the value on top of the fix.

Why CI never caught either

build-develop.yml's Smoke Tests job synthesizes its own docker-compose.test.yml with no named volume and its own inline env — only init.sql comes from the repo. The shipped compose files are never executed by CI, so persistence and .env handling are both unexercised. That is why 66 checks pass green against compose files that cannot start. Worth a follow-up ticket to point the smoke job at the real compose file; not in this PR's scope.

Testing

No Docker daemon in this environment, so this was verified with docker compose config, which resolves interpolation and merge semantics without one:

  • All four compose files parse and validate, plus the base + dev override pair
  • PGDATA matches the volume mount in all three db services (asserted programmatically, not eyeballed)
  • With default .env: DATABASE_URL and POSTGRES_PASSWORD agree on truehour
  • With a custom password in .env: DATABASE_URL now resolves to postgresql://truehour:s3cret-long-password@db:5432/truehour instead of being dropped — the exact failure FATH-8 describes

Not verified here: an actual docker compose up. The daemon isn't available in this container, so the end-to-end proof is the reporter running it.

Two defects that together made the documented setup path fail on a clean
machine. Both affect every compose file that defines the service.

PGDATA (FATH-7): postgres:18 moved PGDATA from /var/lib/postgresql/data
to /var/lib/postgresql/<major>/docker, but the compose files still mount
the pre-18 path. The entrypoint refuses to boot when it finds a mount at
the legacy path that PGDATA no longer points to, so db loops on
Restarting(1) and the api never starts. Pin PGDATA to the mounted path.

The quieter half: had the entrypoint not errored, PGDATA would resolve
inside the container layer while the named volume sat unused, and the
database would be silently discarded on every `docker compose down`.

PGDATA is pinned rather than re-rooting the mount to /var/lib/postgresql
deliberately: anyone holding a volume written by an older image build has
their data at the volume root, and moving the mount would strand it while
initdb quietly created a fresh cluster alongside. Pinning works for an
empty volume and a populated legacy one alike, and migrates nobody.

DATABASE_URL (FATH-8): the api service hardcoded DATABASE_URL in its
environment block, which takes precedence over env_file, so the value in
.env was loaded and silently discarded. Since db does honour
POSTGRES_PASSWORD from the same file, following .env.example's own
instruction to change the password broke authentication. Make the
hardcode a ${DATABASE_URL:-...} default so .env can override it.

Verified with `docker compose config` on all four files, and on the
base+dev override pair: all valid, PGDATA matches the mount in all three
db services, and a custom password in .env now propagates into
DATABASE_URL instead of being dropped.

Tickets: FATH-7, FATH-8

Co-Authored-By: Claude <noreply@anthropic.com>
@ryakel
ryakel merged commit 9a3b2b6 into develop Aug 26, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants