--- name: containerizing-services description: Containerizes backend services with production-grade Dockerfiles — multi-stage builds, pinned base images, non-root users, healthchecks, and small images. Use when the user asks to write or review a Dockerfile, dockerize or containerize a service or app, shrink a Docker image, or fix container build or security issues. Do not use for Kubernetes manifests or orchestration, docker-compose service topology, or CI pipeline design (shipping-with-ci-cd). --- # Containerizing Services ## When to use / when NOT to use - **Use for:** writing or reviewing Dockerfiles, image size/security hardening, build-cache problems. - **Do NOT use for:** Kubernetes/orchestration, compose topology, CI pipelines (shipping-with-ci-cd), or app code changes. ## Core rules 1. **Multi-stage: build heavy, run slim.** Compile/install in a build stage; copy only artifacts into the runtime stage. - ✅ `FROM node:22.4-slim AS build` … `FROM node:22.4-slim` + `COPY --from=build /app/dist ./dist` - ❌ One stage that ships compilers, dev deps, and source history. 2. **Pin base images.** Exact tag minimum; digest for production. - ✅ `FROM python:3.12.4-slim@sha256:…` - ❌ `FROM python:latest` 3. **Run as non-root.** Create a user and switch: `USER app`. Root-only containers fail most cluster policies and widen every exploit. 4. **Order layers for cache.** Copy dependency manifests and install BEFORE copying source, so code edits don't bust the dependency layer. - ✅ `COPY package*.json ./` → `RUN npm ci` → `COPY . .` - ❌ `COPY . .` first (every commit reinstalls everything). 5. **One process per container.** No supervisord bundles; sidecars belong to the orchestrator. Logs go to stdout/stderr only — never files inside the container. 6. **Ship a `.dockerignore`.** At minimum: `.git`, `node_modules`/venvs, `.env*`, secrets, test fixtures. Never `COPY` a secret; pass at runtime. 7. **Define `HEALTHCHECK`** (or document the orchestrator probe) hitting a real readiness endpoint, not `/`. 8. **Default slim/distroless; alpine only knowingly.** musl breaks some native wheels/binaries — use alpine only after the app is verified on it. ## Workflow 1. Pick runtime base (`:-slim` default; distroless escape hatch for static binaries). 2. Write `.dockerignore` before the Dockerfile. 3. Write multi-stage Dockerfile per rules 1–8. 4. Build: `docker build -t svc:dev .` — rebuild after touching one source file and confirm the dependency layer is cached (`CACHED` in output). 5. Validate: `docker run --rm svc:dev id -u` returns non-zero UID; `docker inspect --format='{{.Config.Healthcheck}}' svc:dev` is set; `docker run --rm --read-only svc:dev` starts (add tmpfs mounts if the app needs scratch dirs). 6. Check size: `docker images svc:dev` — if the runtime image exceeds ~2× the artifact size, find what leaked in (`docker history svc:dev`). ## Edge cases & failure modes - **Native deps fail on slim** → install build tools in the build stage only (`apt-get install -y --no-install-recommends build-essential`), never in runtime. - **Secrets needed at build time** → `RUN --mount=type=secret,id=npm_token …` (BuildKit); never `ARG` a secret (it persists in history). - **Image works locally, fails in cluster as non-root** → files owned by root; `COPY --chown=app:app`. - **Timezone/CA errors in distroless** → use the `:debug` or cc variant, or copy `ca-certificates` from the build stage. ## References Deeper recipes and gotchas: see [references/patterns.md](references/patterns.md).