Files
familienarchiv/docs/adr/009-standalone-compose-not-overlay.md
Marcel 59bc81d353 docs(adr): ADR-009 standalone docker-compose.prod.yml, not overlay
Records the decision to make docker-compose.prod.yml a fully self-contained
file rather than an overlay over docker-compose.yml. Captures the cost
(env-var duplication across dev and prod files) and the benefit (single
file the reviewer can hold in their head, no Compose merge-rule
surprises, automatic project-name namespacing for cohabiting staging +
production on one host).

Surfaces the retirement of the earlier overlay narrative in
docs/infrastructure/production-compose.md so a future maintainer does
not reverse the choice out of ignorance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:14:58 +02:00

4.8 KiB

ADR-009: Standalone docker-compose.prod.yml, not an overlay

Status

Accepted

Context

The repository's docker-compose.yml is a development stack: every service is built locally, ports are exposed on 0.0.0.0 for dev tooling, the frontend runs npm run dev with hot-reload, the backend is spring-boot:run with the dev profile, and there is no Caddy, no archiv-app service account, no admin-credential lock-in, no healthcheck-gated startup sequence. The dev stack reflects "single developer on a laptop", not "production on a single VPS".

The pre-merge design (issue #497, comment #8331) sketched two ways to add a production stack:

  1. Overlay — keep docker-compose.yml as the base, add docker-compose.prod.yml as a -f overlay (docker compose -f docker-compose.yml -f docker-compose.prod.yml up). Compose merges the two files at runtime.
  2. Standalone — make docker-compose.prod.yml a fully self-contained file that does not reference or merge with docker-compose.yml at all. Project-name namespacing (-p archiv-production, -p archiv-staging) keeps multi-environment deploys clean on a single host.

The earlier docs/infrastructure/production-compose.md notes assumed overlay because the original plan was to remove MinIO in production (replace with Hetzner Object Storage), so the prod file would only need to remove one service and add a few. With MinIO retained (see ADR-010), the prod stack diverges from dev in essentially every service: build vs pre-built image, target stage, port binding, env vars, healthcheck, restart policy, mem_limit, profile gating, service account, depends_on chain. Overlay would mostly be override: blocks that nullify the dev defaults — a fragile inversion.

Decision

docker-compose.prod.yml is standalone. Production and staging both run it directly:

production: docker compose -f docker-compose.prod.yml -p archiv-production --env-file .env.production ...
staging:    docker compose -f docker-compose.prod.yml -p archiv-staging    --env-file .env.staging --profile staging ...

Environment isolation is achieved via the Docker Compose project name (-p). Volumes, networks, and containers are namespaced by the project name, so production and staging cohabit cleanly on the same host without interfering.

The dev docker-compose.yml is unchanged — docker compose up still works for developers, and its frontend service now specifies target: development explicitly so the new multi-stage Dockerfile builds the right stage.

Alternatives Considered

Alternative Why rejected
Overlay (-f base.yml -f prod.yml) With MinIO retained and most services differing across nearly every field, the overlay would consist mostly of override: blocks that null out dev defaults. Compose's merge semantics for nested keys (env, ports, healthcheck) are sharp — silent merges of port mappings, env-var entries, and depends_on edges cost reviewer hours. Standalone is one file the reader can hold in their head.
Two fully separate files (dev + prod) but with shared YAML anchors via extends: extends: works across files but is a niche feature and is increasingly discouraged in compose v2. Reviewer load is higher than reading two flat files.
Generate prod compose from a template at deploy time (e.g. ytt, kustomize) Adds a build-time step and a new tool to the operator toolchain. Justified for a fleet of 10+ environments; overkill for production + staging on one host.
Single compose file with environment-specific profiles Compose profiles select which services run, not which configuration a service runs with. Using profiles to swap "build locally" vs "pull image" would smear dev and prod across one file.

Consequences

  • The prod file can be read top-to-bottom without cross-referencing docker-compose.yml. Onboarding and review cost drops.
  • Volume namespacing is automatic (archiv-production_postgres-data, archiv-staging_postgres-data) — no manual volumes: aliasing.
  • Dev compose churn (e.g. swapping a dev port) cannot accidentally affect production. The two files are independent.
  • The cost is duplication: identical environment variables (e.g. POSTGRES_DB: archiv) appear in both files. This duplication is bounded — there is no incentive to add more services that exist in both — and the alternative (overlay) carries its own duplication via override: boilerplate.
  • The retired docs/infrastructure/production-compose.md narrative is trimmed to a pointer at the live files. The cost/sizing rationale is preserved there.

Future Direction

If the deployment fleet ever grows beyond two environments on one host (e.g. add a demo environment, or shard staging across two VPS for load testing), revisit the templating decision. At three+ environments the duplication starts to bite and a template engine (kustomize or ytt) becomes attractive.