Files
PersonalBlog/compose.yaml
T
bot-implementer dce6cac05b
CI / Frozen lockfile install (push) Successful in 42s
CI / Secrets not embedded (E00-S02-T08) (push) Successful in 28s
CI / Database-postgres import isolation (E00-S03-T02) (push) Successful in 24s
CI / Migration ledger (E00-S03-T03) (push) Successful in 42s
CI / Migration advisory lock (E00-S03-T04) (push) Successful in 48s
CI / Migration failure diagnostic (E00-S03-T05) (push) Successful in 46s
CI / App readiness after migrations (E00-S03-T06) (push) Successful in 1m4s
CI / Field-specific startup errors (E00-S04-T02) (push) Successful in 1m5s
CI / Secret redaction from logs (E00-S04-T03) (push) Successful in 1m6s
CI / Env adapter owns process.env (E00-S04-T04) (push) Successful in 1m12s
CI / Compose config (E00-S03-T01) (push) Successful in 26s
CI / TypeBox/Ajv config schema (E00-S04-T01) (push) Successful in 1m4s
CI / .env.example placeholders only (E00-S04-T05) (push) Successful in 29s
[E00-S04-T05] .env.example contains placeholders only (#404)
Co-authored-by: bot-implementer <bot-implementer@fabrika.internal>
2026-08-30 05:47:42 +00:00

127 lines
6.3 KiB
YAML

# 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 (the committed .env.example template, E00-S04-T05,
# lists the overridable variables with placeholder values).
# Since E00-S04-T02 the app validates its required settings at startup: the
# admin-session secret `EPPP_SESSION_SECRET` (the schema's required field,
# Security-and-Operations §32/§26) is provided here with a dev-only default —
# override it via a `.env` file / shell environment for anything beyond local
# development.
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
# E00-S04-T02: the app's required admin-session secret (the schema's
# required field, EPPP_SESSION_SECRET per Security-and-Operations
# §32/§26) — dev-only default (>= 32 chars), override via .env / shell.
EPPP_SESSION_SECRET: ${EPPP_SESSION_SECRET:-eppp-local-session-secret-change-me-0123456789}
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: