Skip to content

Docker foundations

  1. Multi-Stage Builds: Build artifacts in a fully-equipped builder stage; copy only the compiled binaries and production dependencies into a clean final image.
  2. Non-Root Execution: Explicitly define and drop to an unprivileged user (USER node or USER nonroot).
  3. Minimal Base Images: Prefer Alpine, Debian-slim, or Google Distroless images over full Ubuntu or Debian images.
  4. Read-Only Root Filesystem: Configure runtime mounts as read-only whenever possible, writing temporary data only to ephemeral tmpfs mounts.
  5. Deterministic Pinning: Pin base image digests or explicit patch versions to avoid unexpected upstream drift.

Standard Multi-Stage Dockerfile Pattern (Node.js Example)

Section titled “Standard Multi-Stage Dockerfile Pattern (Node.js Example)”

Here is a hardened, production-ready multi-stage Dockerfile:

# ----------------------------------------------------
# Stage 1: Dependency resolution and compilation
# ----------------------------------------------------
FROM node:22-alpine AS builder
WORKDIR /app
# Copy dependency manifests first for layer caching
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Copy source code and compile
COPY . .
RUN npm run build && npm prune --production
# ----------------------------------------------------
# Stage 2: Minimal runtime image
# ----------------------------------------------------
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Install dumb-init to properly forward Unix signals and reap zombies
RUN apk add --no-cache dumb-init
# Use pre-existing unprivileged node user
USER node
# Copy built application and production dependencies from builder stage
COPY --chown=node:node --from=builder /app/package.json ./
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/dist ./dist
EXPOSE 3000
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "dist/server/entry.mjs"]

When running services locally or on standalone Docker engines, enforce security bounds in docker-compose.yml:

services:
web:
image: example-app:1.0.0
restart: unless-stopped
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M

Automated Container Vulnerability Scanning

Section titled “Automated Container Vulnerability Scanning”

Integrate Trivy or Docker Scout into your CI pipeline to catch CVEs prior to image registry pushes:

Terminal window
# Scan Docker image locally with Trivy
trivy image --severity HIGH,CRITICAL example-app:1.0.0