From 59bc81d353d5ca1a1645fe33e42b2fb678b7b16f Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 11 May 2026 13:14:58 +0200 Subject: [PATCH] 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 --- .../adr/009-standalone-compose-not-overlay.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/adr/009-standalone-compose-not-overlay.md diff --git a/docs/adr/009-standalone-compose-not-overlay.md b/docs/adr/009-standalone-compose-not-overlay.md new file mode 100644 index 00000000..e861fe47 --- /dev/null +++ b/docs/adr/009-standalone-compose-not-overlay.md @@ -0,0 +1,50 @@ +# 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.