Resolve the security review of #389 (findings 1-4): - .dockerignore: every env/credential pattern is now **/-prefixed (**/.env, **/.env.*, **/node_modules, **/.npmrc, ..., **/secrets, **/*.pem, **/*.key, ...) and the redundant 'secrets/' line is dropped. Docker's matcher (moby/patternmatcher) anchors slash-less patterns to the context root, so the bare forms excluded nothing under apps/server/; **/ matches the root AND any nested depth. (finding 1, 3) - tests/secrets-not-embedded.test.mjs: the dockerignore matcher is now a faithful port of moby/patternmatcher (filepath.Clean + anchored full-path match + parent-directory propagation), not gitignore basename semantics; asserts nested example paths (apps/server/.npmrc, config/server.key, apps/server/secrets/...) are excluded; requires no redundant equivalent patterns verbatim; adds mutation probes for bare-pattern and duplicate-pattern regressions. (finding 2, 3) - apps/server/Dockerfile + compose.yaml: guarantee restated precisely (credential files excluded at the context root AND at any depth). - .gitea/workflows/ci.yml: new job runs 'node --test tests/secrets-not-embedded.test.mjs' on every PR; the docker-gated layer-scan probe runs where a daemon exists, skips cleanly otherwise. (finding 4) - tests/compose-config.test.mjs: .dockerignore presence list updated to the **/-prefixed forms (node_modules, .env). Tested: secrets suite 16 tests -> 15 pass / 1 docker-gated skip / 0 fail; full suite 101 pass / 12 fail / 8 skip, failures identical to clean main (env-dependent pnpm/Node-24 suites); matcher port verified against the moby/patternmatcher evidence table.
114 lines
5.4 KiB
YAML
114 lines
5.4 KiB
YAML
# EPPP Docker Compose baseline — [E00-S02-T01..T08]
|
|
#
|
|
# `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 on Debian bookworm — the documented runtime target
|
|
# (Technology-Stack §5.2/§5.4/§6.2, golden tuple §7; no Alpine drift).
|
|
image: postgres:18-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:
|