fix(infra): make docker compose actually start and persist data - #39
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tickets: FATH-7, FATH-8
Two defects that together make the documented setup path —
cp .env.example .env && docker compose up -d --build— fail on a clean machine. Found by actually running it.FATH-7:
dbcannot start (postgres:18 mount)postgres:18movedPGDATAfrom/var/lib/postgresql/datato 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 setPGDATA, so the entrypoint refuses to boot:dbloops onRestarting (1), andapinever starts withdependency 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_VERSIONat 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:
PGDATAresolves inside the container layer while the named volume sits unused, so the stack looks healthy and silently discards the database on everydocker compose down. The loud failure is the good case.Why
PGDATAis 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 whileinitdbquietly 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 futurepg_upgrade --linkmore awkward — the right side of the trade for a single-user self-hosted app.FATH-8:
.envcredentials silently ignoredThe
apiservice hardcodedDATABASE_URLin itsenvironment:block, which takes precedence overenv_file:. So the value in.envwas loaded and discarded — whiledbdid honourPOSTGRES_PASSWORDfrom the same file.Net effect: following
.env.example's own security instruction to change the password broke authentication. Copying.env.exampleverbatim failed too, sinceCHANGE_ME_BEFORE_PRODUCTION≠truehour. The only working.envwas one that ignored the security advice.Fixed by making the hardcode a
${DATABASE_URL:-...}default.docker-compose.dev.ymlis 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 owndocker-compose.test.ymlwith no named volume and its own inline env — onlyinit.sqlcomes from the repo. The shipped compose files are never executed by CI, so persistence and.envhandling 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:base + devoverride pairPGDATAmatches the volume mount in all threedbservices (asserted programmatically, not eyeballed).env:DATABASE_URLandPOSTGRES_PASSWORDagree ontruehour.env:DATABASE_URLnow resolves topostgresql://truehour:s3cret-long-password@db:5432/truehourinstead of being dropped — the exact failure FATH-8 describesNot 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.