# EPPP Docker Compose baseline — [E00-S02-T01..T08] + [E00-S03-T01] # # `docker compose up -d` starts both the database (PostgreSQL) and the # application (@personal-blog/server). Rollback: `docker compose down`. # # PostgreSQL health gate (T02): the `db` service carries a `pg_isready` # healthcheck and the `app` service depends on it with # `condition: service_healthy`, so the application does not start until the # database is accepting connections. # # Application health endpoint (T03): the app serves `GET /health` (HTTP 200 + # `{"status":"ok"}`) on port 3000, so the app container stays up and the # health endpoint succeeds once the stack is running. # # Database volume persistence (T04): the `db` service mounts the named volume # `db-data` at PostgreSQL's data directory (`/var/lib/postgresql/data`), so # the database survives `docker compose restart` (restart) and # `docker compose down` + `docker compose up -d` (recreate, which discards the # container filesystem). Reset the data with `docker compose down -v`. # # Non-root execution (T05): the app image's runtime stage runs as the official # Node image's non-root `node` user (see apps/server/Dockerfile — `USER node`), # so the app container does not run with root privileges. No Compose-level # `user:` override is needed: the image's USER is inherited by the container. # # Read-only root filesystem (T06): the `app` service sets `read_only: true`, so # the container's root filesystem is mounted read-only — a write anywhere on it # is denied. Writable paths are limited to declared mounts and tmpfs: the app # declares a `tmpfs` at `/tmp` and no writable volume/bind mounts, so `/tmp` is # the only writable path. Rollback: drop `read_only`/`tmpfs` from the `app` # service. # # Multi-arch build targets (T07): the `app` service's `build.platforms` list # declares `linux/amd64` and `linux/arm64` (Compose Build spec `platforms`), so # `docker compose build` produces a multi-platform image for both # architectures. `docker compose up` still builds and runs the host platform, # so local runs and the T01..T06 real-stack probes are unaffected. Rollback: # drop the `platforms` list from the `app` build config. # # Secrets not embedded (T08): the app image carries no secrets — the committed # apps/server/Dockerfile declares no secret-bearing ARG/ENV instruction and # copies only fixed, non-secret paths, and the committed .dockerignore excludes # env and credential files from the build context at the context root AND at # any nested depth (`**/`-prefixed patterns — Docker's matcher anchors # slash-less patterns to the context root). Runtime credentials are # injected here, at run time, via service `environment` values (the app's # DATABASE_URL, the db service's POSTGRES_* defaults) — they live in # Compose/deploy config, never in the image. Rollback: rebuild the image after # removing any embedded secret. Tests: tests/secrets-not-embedded.test.mjs. # # All values have defaults so `docker compose up -d` works from a clean clone # without a .env file (a committed .env.example template lands in E00-S04). services: db: # PostgreSQL 18.6 on Debian bookworm — the documented runtime target # (Technology-Stack §5.2/§5.4/§6.2, golden tuple §7; no Alpine drift), # pinned to the exact 18.6 minor (E00-S03-T01) so the database container # is reproducible and exposes the expected PostgreSQL version. Rollback: # revert `image` to `postgres:18-bookworm`. image: postgres:18.6-bookworm environment: POSTGRES_DB: ${POSTGRES_DB:-eppp} POSTGRES_USER: ${POSTGRES_USER:-eppp} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-eppp} ports: - "${POSTGRES_PORT:-5432}:5432" # T04: persist the database in the named `db-data` volume (PostgreSQL data # directory), so data survives `docker compose restart` and `docker # compose down` + `up -d` (recreate). `docker compose down -v` resets it. volumes: - db-data:/var/lib/postgresql/data # Health gate for the app service (T02): probe the same credentials the db # service was created with. `$$` defers interpolation to the container, so # POSTGRES_USER/POSTGRES_DB overrides apply to the probe too. healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 5 start_period: 5s app: build: context: . dockerfile: apps/server/Dockerfile # T07: multi-arch build targets — `docker compose build` produces a # multi-platform image for linux/amd64 and linux/arm64 (Compose Build # spec `platforms`). `docker compose up` builds/runs the host platform, # so the T01..T06 real-stack probes are unaffected. platforms: - linux/amd64 - linux/arm64 environment: DATABASE_URL: postgres://eppp:eppp@db:5432/eppp ports: - "${APP_PORT:-3000}:3000" # T02: start only once the database reports healthy (service_healthy), so # the application waits for PostgreSQL before starting. depends_on: db: condition: service_healthy # T06: read-only root filesystem — the container's root filesystem is # mounted read-only (`read_only: true`), so writes are denied everywhere # except the declared mounts/tmpfs below. The app writes nothing else, so # the only writable path is the declared `tmpfs` at `/tmp` (no writable # volumes or bind mounts on this service). read_only: true tmpfs: - /tmp # Named volumes shared across `docker compose` lifecycles. `db-data` (T04) # holds the PostgreSQL data directory and is preserved across restart and # recreate; `docker compose down -v` removes it to reset the database. volumes: db-data: