Build secure, reproducible, and efficient container images using Dockerfiles, `.dockerignore`, runtime users, and clear image validation commands. Use when the story requires container packaging or runtime image changes.
Build secure, reproducible, and efficient container images with clear runtime behavior.
Follow docs/guidelines/shared-operating-policy.md#extension-pack-activation-rule — this skill belongs to the platform-infrastructure extension pack.
Order Dockerfile instructions to maximize cache reuse: immutable operations (FROM, WORKDIR) first, then less-frequently-changed dependencies (package.json), then source code. Group COPY + RUN for dependency installation before source code to preserve cache when code changes. Use build stage separation to avoid polluting cache with build-time dependencies.
Run trivy image <image-name> after build to identify vulnerable packages before deployment. Scan both filesystem (vulnerability DB) and container layers. Compare scan results against organization policy; document CVE triage decisions (accept/remediate/delay). Prefer minimal base images (Alpine, distroless) to reduce attack surface and scan results.
Match base image to workload: Alpine/distroless for compiled languages (Go, Rust) or language-runtime-only (Python, Node containers with prebuilt libs); -slim variants for interpreted languages needing modest system utilities; full distros (Debian) only if specialized tooling required. Pin to digest (node:22-bookworm@sha256:...) for reproducibility in high-assurance contexts; version tag (node:22-bookworm) for flexibility. Document base image end-of-life date.
Separate builder, compiler, and runtime stages: builder stage installs dev dependencies and compile tools; intermediate stage holds compiled artifacts; runtime stage copies only necessary binaries/libraries. Example: Go app builder with full toolchain → scratch image with single binary. Reduces final image 50-90% vs single-stage.
Use docker history <image> to identify largest layers; docker inspect <image> to review layering. Use .dockerignore to exclude source control, test files, documentation (common bloat: node_modules in build context, .git folders). Chain RUN commands in build stage to avoid intermediate layers: RUN apt-get update && apt-get install ... && apt-get clean instead of separate RUNs.
Run as non-root user: create dedicated unprivileged user (UID >1000) with RUN useradd -r -u 10001 appuser && USER appuser. Make data directories writable only if needed; prefer read-only root filesystem where possible. Drop unnecessary capabilities: --cap-drop=NET_RAW if not binding raw sockets. Document any required privilege assumptions in story.
Build separate images or use build args to exclude debug tools from production: multi-stage builder with dev tools, runtime stage without them. Tag dev images with -dev suffix; reserve -latest for production-ready images. Use builder patterns (e.g., Node npm ci --omit=dev) to strip dev dependencies at install time, not post-hoc deletion.
Use semantic versioning (v1.2.3) for releases; git SHA or branch name for development images. Tag production images with both version and digest for immutability: myapp:v1.2.3@sha256:.... Store image provenance metadata in OCI labels: LABEL version=1.2.3 git.sha=abc123 build.date=2025-03-25. Include digest in deployment manifests to guarantee specific image version.
Define explicit ENTRYPOINT and CMD; avoid relying on shell defaults. Use exec form (ENTRYPOINT ["node", "server.js"]) not shell form to ensure signals (SIGTERM) reach the app directly, enabling graceful shutdown. If script-based entrypoint needed (e.g., database migrations before app start), use explicit exec for the final process: exec "$@" at end of init script. Test SIGTERM handling: app must shut down within timeout (30s container stop default).
Document exposed ports explicitly: EXPOSE 8080. Map only necessary ports in docker run/compose. Use port remapping in compose (host:container) if flexibility needed. Distinguish internal communication ports (between containers, unpublished) from external-facing ports. Document hostname resolution strategy: services on same network reach each other by name; external connections use host/port mapping. Avoid --network host unless required (e.g., UDP-based protocols); bridge mode is default and preferred for isolation.
.dockerignore first if needed.HEALTHCHECK only if the runtime/platform expects it and the story scope allows it.# Build the image
docker build -t my-app:dev .
# Run the container with representative environment variables
docker run --rm -p 8080:8080 --env-file .env.local my-app:dev
# Inspect resulting image history and metadata
docker history my-app:dev
docker inspect my-app:dev
FROM node:22-bookworm AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev && useradd -r -u 10001 appuser
USER appuser
EXPOSE 8080
CMD ["node", "dist/server.js"]
Follow the universal checklist at docs/guidelines/shared-operating-policy.md#completion-checklist and confirm all items pass before marking work complete.