Claude Fable 5.1 & GPT-6 Astra packages are live

Docker

Free

Container images that are small, reproducible and safe — multi-stage builds, layer caching, non-root users, signal handling, and what never goes in…

229 lines8.7 KB Gemini DevOps
targetModels
Gemini 3.8 FlashGemini 3.7 FlashGemini 3.1 ProGemini 3 FamilyFuture Gemini Models
name
docker
category
DevOps
description
Container images that are small, reproducible and safe — multi-stage builds, layer caching, non-root users, signal handling, and what never goes in an image.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Gemini: scripts/model-profiles.json -->

#Purpose

Rules for writing Dockerfiles and building images. Three goals, in order:

  1. Correct — the image runs the same everywhere and handles signals properly.
  2. Safe — minimal attack surface, no secrets, non-root.
  3. Fast — cached layers, small final image.

Most Dockerfiles fail the first two while optimising the third.


#Multi-stage builds

dockerfile
# syntax=docker/dockerfile:1
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci        # cached across builds

FROM node:22-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
RUN useradd --system --uid 10001 app
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
USER 10001
EXPOSE 3000
CMD ["node", "dist/server.js"]

The build toolchain, source, test files and development dependencies stay in earlier stages. Only the artefact is copied forward — a smaller image with a smaller attack surface.

Order layers by change frequency: dependency manifests before source. Copying the whole context first means every source edit invalidates the dependency install, and the build never uses its cache.


#Never put secrets in an image

dockerfile
# Every one of these persists in the image history, retrievable with `docker history`
ARG NPM_TOKEN                       # ❌
ENV API_KEY=sk-live-…               # ❌
COPY .env .                         # ❌
RUN echo "$TOKEN" > ~/.npmrc        # ❌ — deleting it later does not remove the layer

Deleting a file in a later layer does not remove it; the earlier layer still contains it. Use build secrets, which are mounted and never written to a layer:

dockerfile
RUN --mount=type=secret,id=npmtoken \
    NPM_TOKEN=$(cat /run/secrets/npmtoken) npm ci

Runtime secrets are injected by the orchestrator at start, never baked in. → Security/secret-management

A .dockerignore is mandatory — without it, .git, .env, node_modules and local credentials are sent to the daemon and frequently end up in the image:

bash
.git
.env*
node_modules
**/*.test.*
Dockerfile*

#Run as a non-root user

The default is root. A container escape or a compromised process then has root on the host namespace.

dockerfile
RUN useradd --system --uid 10001 app
USER 10001                       # numeric UID, so Kubernetes runAsNonRoot can verify it

Additional hardening at runtime:

SettingEffect
readOnlyRootFilesystem: trueWrites only to declared volumes
allowPrivilegeEscalation: falsesetuid binaries cannot escalate
capabilities: { drop: ["ALL"] }Removes every Linux capability
--security-opt no-new-privilegesThe Docker-run equivalent

Use a specific base tag (node:22.4.1-slim) or a digest, never latest. latest makes builds irreproducible — the image that passed CI is not the image that deployed.

Prefer -slim or distroless bases. Fewer packages means fewer CVEs and less to patch; a distroless image has no shell, which also removes the most common post-exploitation foothold.


#Signals and process model

dockerfile
CMD ["node", "dist/server.js"]        # exec form: node is PID 1 and receives SIGTERM
# CMD npm start                       # shell form: sh is PID 1, npm swallows the signal

The shell form wraps the command in /bin/sh -c, so your process is not PID 1 and often never receives SIGTERM. The orchestrator then waits the full grace period and SIGKILLs — every deploy severs in-flight requests.

  • Use the exec form (JSON array) for CMD and ENTRYPOINT.
  • Do not start the process through npm, yarn or a shell wrapper.
  • If your process spawns children, add --init (or tini) so zombies are reaped.
  • Handle SIGTERM in the application: stop accepting connections, drain, exit. → Backend/node

One process per container. Supervisors running several services in one container defeat orchestration, scaling and health checking.


#Build, scan and ship

  • Build once, promote the same digest through environments. Rebuilding per environment means staging and production are different images.
  • Tag with the commit SHA, not only latest, so a deployed image is traceable.
  • Scan in CI (trivy image, grype) and fail on high or critical findings; rebuild regularly to pick up base-image patches.
  • Generate an SBOM and sign the image (cosign) where supply-chain provenance matters. → DevOps/cicd
  • Add a HEALTHCHECK, or configure probes in the orchestrator.
  • Set memory limits and configure the runtime to respect them — a JVM or Node heap sized from host memory will be OOM-killed in a limited container.

#Anti-patterns

Anti-patternWhy it failsFix
FROM node:latestIrreproducible; changes under youPin a version or digest
COPY . . before installing dependenciesCache invalidated by every source editManifests first
No .dockerignore.git, .env, credentials in the imageAdd one
Secrets in ARG/ENV/COPYPersist in image historyBuild secrets; runtime injection
Deleting a secret in a later layerThe earlier layer still has itNever write it
Running as rootEscape gains host rootUSER with a numeric UID
Full OS base imageLarge surface, many CVEsSlim or distroless
Shell-form CMDSignals never reach the processExec form
Starting via npm startnpm swallows SIGTERMInvoke the binary directly
Multiple processes per containerBreaks scaling and health checksOne process
Build tools in the final imageAttack surface and sizeMulti-stage
Rebuilding per environmentStaging and production differPromote one digest
No image scanningKnown CVEs shipScan and gate in CI
Only latest tagsCannot trace what is runningTag with the commit SHA
Runtime heap sized from host memoryOOM-killed under limitsConfigure against the limit

#Checklist

  • Verify: Base images are pinned to a version or digest, never latest
  • Verify: Multi-stage builds keep toolchains and dev dependencies out of the runtime
  • Verify: Layers are ordered so dependency installs stay cached
  • Verify: A .dockerignore excludes VCS metadata, environment files and local artefacts
  • Verify: No secret appears in ARG, ENV, COPY or any layer
  • Verify: Build-time secrets use mounted secrets
  • Verify: The container runs as a non-root numeric UID
  • Verify: Root filesystem is read-only and capabilities are dropped where possible
  • Verify: CMD/ENTRYPOINT use the exec form
  • Verify: The application receives and handles SIGTERM
  • Verify: An init process reaps children where the process spawns them
  • Verify: One process per container
  • Verify: Images are built once and promoted by digest
  • Verify: Images are tagged with the commit SHA
  • Verify: CI scans images and fails on high-severity findings
  • Verify: Health checks or probes are configured
  • Verify: Runtime memory settings respect the container limit

#Anchors (restated last, read last)

The rules that must hold when you stop, repeated here because the end of the context is what you act on:

  • Base images are pinned to a version or digest, never latest
  • Multi-stage builds keep toolchains and dev dependencies out of the runtime
  • Layers are ordered so dependency installs stay cached
  • A .dockerignore excludes VCS metadata, environment files and local artefacts
  • No secret appears in ARG, ENV, COPY or any layer
  • Build-time secrets use mounted secrets

Before reporting done, prove the module still imports — run the line for this stack and paste its output:

bash
python -c "import <package>"          # Python: the package you changed
node -e "require('./<entry>')"       # Node CJS, or: node --input-type=module -e "import './<entry>.js'"
go build ./...                        # Go