Docker Architecture & Container Standards
Targets Docker Engine 29, Compose v2, BuildKit (default). Commands use docker compose (v2 plugin, no hyphen). File names use docker-compose.yaml. Per-language Dockerfiles and BuildKit/multi-arch/Trivy commands in RECIPES.md; pinned tool versions in STACK.md.
1. Dockerfile fundamentals
- Always multi-stage. A build stage (toolchain + sources) and a final runtime stage that copies only the artifacts. Never ship the toolchain in the runtime image.
- Layer order = least → most volatile. Pin OS deps first, then language deps, then source. Source code changes invalidate the fewest layers possible.
.dockerignore is mandatory. Excludes .git/, node_modules/, .venv/, build outputs, secrets, IDE files. Bad .dockerignore is the most common cause of bloated images and accidentally-leaked secrets.
- Non-root USER. Final stage runs as a dedicated, non-root user (UID ≥ 10000). Distroless
:nonroot tag handles this; for Debian-based, useradd -u 10001 -r app && USER 10001.
HEALTHCHECK on every long-running service. Use the simplest possible probe (HTTP /healthz, pg_isready, etc.). Compose depends_on conditions depend on healthchecks being correct.
- No
RUN apt-get update without install + cleanup in the same layer: RUN apt-get update && apt-get install -y --no-install-recommends X && rm -rf /var/lib/apt/lists/*.
2. Base image selection
Per-language defaults:
| Language |
Default base |
Why |
| Go |
gcr.io/distroless/static-debian12:nonroot |
Static binary, ~2 MB image, non-root by default, no shell (smaller attack surface). Reach for scratch only after auditing CA certs + tzdata yourself. |
| Python |
python:3.14-slim (debian slim) |
uv in a builder stage, copy .venv to runtime. Avoid alpine — musl breaks several scientific wheels. |
| Node |
node:22-slim (debian slim) |
Alpine breaks too many native modules. LTS only in production. |
Always pin by digest in production: FROM python:3.14-slim@sha256:abc.... Tags are mutable; digests aren't. Refresh digests via Renovate / Dependabot.
3. BuildKit features
BuildKit is the default builder in Docker 29 — use cache mounts (survive layer invalidation), secret mounts (never COPY secrets into layers), bind mounts (read source without COPY), and here-docs (multi-line scripts) deliberately. Snippets in RECIPES.md.
4. Image security
- Digest-pin bases in production Dockerfiles. Renovate updates them automatically; review the diff.
- No secrets baked in. Build-time secrets go through
--mount=type=secret. Runtime secrets come from the orchestrator (env, mounted file, secrets manager).
- Drop capabilities at runtime when possible (
--cap-drop=ALL --cap-add=NET_BIND_SERVICE).
- Read-only root filesystem for stateless services (
--read-only + tmpfs for /tmp).
- Distroless
:nonroot or explicit USER — never run as root in the final stage.
- Single-process containers. No init system unless the app forks (then use
--init / tini).
- Scan every image in CI (see §10).
5. Multi-arch builds
Always build linux/amd64 + linux/arm64. Cloud is largely arm64-friendly now (Graviton, Ampere); local dev on Apple Silicon is arm64-native. docker buildx build --platform linux/amd64,linux/arm64 ... — full command + cache options in RECIPES.md.
6. Compose patterns (v2)
7. Runtime defaults
- Resource limits on every service (
deploy.resources.limits.memory, cpus). Unbounded containers eat hosts.
- Logging driver:
json-file with size + count rotation, or journald on Linux hosts. Production typically forwards to a log aggregator.
- Restart policy:
unless-stopped for long-running services; no for batch jobs.
- Init process: add
--init (or init: true in Compose) when the app spawns child processes — prevents zombie processes.
- TZ: set
TZ=Etc/UTC explicitly in the image; never rely on host timezone.
8. Dev vs prod
docker-compose.yaml — production-shaped baseline (images by digest, no source bind mounts, prod env defaults).
docker-compose.override.yaml — dev-only additions: bind-mount source for hot reload, expose ports for debuggers, pull secrets from .env.local.
- Compose loads both automatically with
docker compose up. To run prod-only, use docker compose -f docker-compose.yaml up (skip the override).
- Never bake dev conveniences into the main file. That's how mounting
./ into prod ships.
9. Registry & tagging
- Production deploys reference image digests (
@sha256:...), not tags. Tags are for humans; digests are for machines.
- Human-facing tags follow semver (
1.2.3) plus a moving latest and 1.2 major/minor aliases for local convenience.
- CI also pushes
:<short-sha> for traceability — easy to roll back to a specific commit.
- Sign images with Cosign for prod-bound registries. Verify on pull in the deployment platform.
10. Vulnerability scanning — Trivy
- Default scanner:
aquasecurity/trivy. Open-source, fast, scans images + filesystems + IaC.
- CI step on every push (
trivy image --severity HIGH,CRITICAL --exit-code 1 ...) — full command in RECIPES.md.
- Ignore file (
.trivyignore) for documented, accepted exceptions — never silent allowlists.
- SBOM:
trivy image --format spdx-json --output sbom.json ... — attach to releases. Required for supply-chain compliance.
11. Language-specific recipes
Reference Dockerfiles for Go (distroless), Python (uv, debian slim), and Node (debian slim) live in RECIPES.md.
New project checklist
When scaffolding a new project, use CHECKLIST.md to walk through decisions on restart policies, resource limits, healthchecks, security, and registry setup. The checklist explicitly prompts on restart policy (unless-stopped for long-running services, no for batch jobs).
1---2name: docker-architect3description: Docker standards — multi-stage builds, per-language base defaults (distroless Go, slim Python/Node), BuildKit cache mounts, non-root, multi-arch amd64+arm64, digest-pinned bases, Trivy scanning, Compose v2. Use when writing or reviewing Dockerfiles or Compose files.4---56# Docker Architecture & Container Standards78Targets **Docker Engine 29**, **Compose v2**, **BuildKit** (default). Commands use `docker compose` (v2 plugin, no hyphen). File names use `docker-compose.yaml`. Per-language Dockerfiles and BuildKit/multi-arch/Trivy commands in [RECIPES.md](RECIPES.md); pinned tool versions in [STACK.md](STACK.md).910## 1. Dockerfile fundamentals1112- **Always multi-stage.** A build stage (toolchain + sources) and a final runtime stage that copies only the artifacts. Never ship the toolchain in the runtime image.13- **Layer order = least → most volatile.** Pin OS deps first, then language deps, then source. Source code changes invalidate the fewest layers possible.14- **`.dockerignore` is mandatory.** Excludes `.git/`, `node_modules/`, `.venv/`, build outputs, secrets, IDE files. Bad `.dockerignore` is the most common cause of bloated images and accidentally-leaked secrets.15- **Non-root USER.** Final stage runs as a dedicated, non-root user (UID ≥ 10000). Distroless `:nonroot` tag handles this; for Debian-based, `useradd -u 10001 -r app && USER 10001`.16- **`HEALTHCHECK`** on every long-running service. Use the simplest possible probe (HTTP `/healthz`, `pg_isready`, etc.). Compose `depends_on` conditions depend on healthchecks being correct.17- **No `RUN apt-get update` without install + cleanup in the same layer:** `RUN apt-get update && apt-get install -y --no-install-recommends X && rm -rf /var/lib/apt/lists/*`.1819## 2. Base image selection2021Per-language defaults:2223| Language | Default base | Why |24|---|---|---|25| **Go** | `gcr.io/distroless/static-debian12:nonroot` | Static binary, ~2 MB image, non-root by default, no shell (smaller attack surface). Reach for `scratch` only after auditing CA certs + tzdata yourself. |26| **Python** | `python:3.14-slim` (debian slim) | `uv` in a builder stage, copy `.venv` to runtime. Avoid alpine — musl breaks several scientific wheels. |27| **Node** | `node:22-slim` (debian slim) | Alpine breaks too many native modules. LTS only in production. |2829**Always pin by digest in production:** `FROM python:3.14-slim@sha256:abc...`. Tags are mutable; digests aren't. Refresh digests via Renovate / Dependabot.3031## 3. BuildKit features3233BuildKit is the default builder in Docker 29 — use **cache mounts** (survive layer invalidation), **secret mounts** (never `COPY` secrets into layers), **bind mounts** (read source without `COPY`), and **here-docs** (multi-line scripts) deliberately. Snippets in [RECIPES.md](RECIPES.md).3435## 4. Image security3637- **Digest-pin bases** in production Dockerfiles. Renovate updates them automatically; review the diff.38- **No secrets baked in.** Build-time secrets go through `--mount=type=secret`. Runtime secrets come from the orchestrator (env, mounted file, secrets manager).39- **Drop capabilities** at runtime when possible (`--cap-drop=ALL --cap-add=NET_BIND_SERVICE`).40- **Read-only root filesystem** for stateless services (`--read-only` + tmpfs for `/tmp`).41- **Distroless `:nonroot` or explicit `USER`** — never run as root in the final stage.42- **Single-process containers.** No init system unless the app forks (then use `--init` / `tini`).43- **Scan every image in CI** (see §10).4445## 5. Multi-arch builds4647Always build `linux/amd64` + `linux/arm64`. Cloud is largely arm64-friendly now (Graviton, Ampere); local dev on Apple Silicon is arm64-native. `docker buildx build --platform linux/amd64,linux/arm64 ...` — full command + cache options in [RECIPES.md](RECIPES.md).4849## 6. Compose patterns (v2)5051- **File name:** `docker-compose.yaml` (long extension), with `docker-compose.override.yaml` for dev-only additions. Compose auto-merges them.52- **Command:** `docker compose up` (v2 plugin, integrated). The legacy `docker-compose` standalone binary is deprecated — don't use it.53- **No `version:` key.** Compose v2 ignores it; remove from any file you touch.54- **`depends_on` with conditions:**55 ```yaml56 services:57 app:58 depends_on:59 db:60 condition: service_healthy61 ```62 This requires `db` to define a working `healthcheck`. `depends_on` without `condition:` only orders startup — doesn't wait for readiness.63- **Named volumes for stateful data** (`db-data:`), bind mounts only for source-code hot-reload in dev.64- **Networks:** declare them explicitly; don't rely on the default network for anything non-trivial.65- **Secrets and configs:** use `secrets:` and `configs:` top-level blocks for production-shaped local runs.6667## 7. Runtime defaults6869- **Resource limits** on every service (`deploy.resources.limits.memory`, `cpus`). Unbounded containers eat hosts.70- **Logging driver:** `json-file` with size + count rotation, or `journald` on Linux hosts. Production typically forwards to a log aggregator.71- **Restart policy:** `unless-stopped` for long-running services; `no` for batch jobs.72- **Init process:** add `--init` (or `init: true` in Compose) when the app spawns child processes — prevents zombie processes.73- **TZ:** set `TZ=Etc/UTC` explicitly in the image; never rely on host timezone.7475## 8. Dev vs prod7677- `docker-compose.yaml` — production-shaped baseline (images by digest, no source bind mounts, prod env defaults).78- `docker-compose.override.yaml` — dev-only additions: bind-mount source for hot reload, expose ports for debuggers, pull secrets from `.env.local`.79- Compose loads both automatically with `docker compose up`. To run *prod-only*, use `docker compose -f docker-compose.yaml up` (skip the override).80- **Never bake dev conveniences into the main file.** That's how mounting `./` into prod ships.8182## 9. Registry & tagging8384- **Production deploys reference image digests** (`@sha256:...`), not tags. Tags are for humans; digests are for machines.85- **Human-facing tags follow semver** (`1.2.3`) plus a moving `latest` and `1.2` major/minor aliases for local convenience.86- **CI also pushes `:<short-sha>`** for traceability — easy to roll back to a specific commit.87- **Sign images** with Cosign for prod-bound registries. Verify on pull in the deployment platform.8889## 10. Vulnerability scanning — Trivy9091- **Default scanner: `aquasecurity/trivy`.** Open-source, fast, scans images + filesystems + IaC.92- **CI step on every push** (`trivy image --severity HIGH,CRITICAL --exit-code 1 ...`) — full command in [RECIPES.md](RECIPES.md).93- **Ignore file** (`.trivyignore`) for documented, accepted exceptions — never silent allowlists.94- **SBOM:** `trivy image --format spdx-json --output sbom.json ...` — attach to releases. Required for supply-chain compliance.9596## 11. Language-specific recipes9798Reference Dockerfiles for **Go (distroless)**, **Python (uv, debian slim)**, and **Node (debian slim)** live in [RECIPES.md](RECIPES.md).99100## New project checklist101102When scaffolding a new project, use [CHECKLIST.md](CHECKLIST.md) to walk through decisions on restart policies, resource limits, healthchecks, security, and registry setup. The checklist explicitly prompts on **restart policy** (`unless-stopped` for long-running services, `no` for batch jobs).