iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Image Best Practices

Building production-grade Docker images is a craft: minimal base, deterministic builds, layer caching, non-root user, signed releases, and a clear policy on what goes in and what stays out. Smaller, simpler images mean faster deploys, smaller CVE surface, and less surprise.

Slim base, multi-stage, security, signing

EXAMPLE
# 1) Pick the right base
# • alpine          — tiny (~5 MB) but musl libc differs from glibc; some native modules trip
# • debian-slim     — small (~80 MB), familiar libc, broad compatibility
# • distroless      — no shell, no package manager; security gold standard
# • scratch         — empty; only for static binaries (Go, Rust release builds)
#
# Always pin by digest, not tag, for reproducible builds:
FROM node:20.10.0-alpine@sha256:abc123...

# 2) Multi-stage — separate build vs runtime
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

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

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist          ./dist
COPY --from=build /app/node_modules  ./node_modules
COPY --from=build /app/package.json  ./
USER node
EXPOSE 3000
CMD ['node', 'dist/server.js']

# 3) Optimise layer caching — put stable things first
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./       # rarely changes — cached layer
RUN npm ci                   # also cached
COPY src/ ./src/             # changes often — invalidates from here
RUN npm run build

# Code changes don't bust the npm install layer.

# 4) .dockerignore — keep build context lean
node_modules
.git
.env*
coverage
build
dist
logs
*.log
.DS_Store
Readme.md
.idea
.vscode

# Smaller context → faster builds AND no accidental secret leaks.

# 5) Non-root user
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ['node', 'server.js']

# Most images include a non-root user (node, nginx, www-data). USE IT.
# Custom:
RUN addgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app
USER app

# 6) Readonly root filesystem in Kubernetes
securityContext:
    readOnlyRootFilesystem: true
# Pair with writable volumes for tmp/cache:
volumeMounts:
    - { name: tmp, mountPath: /tmp }
volumes:
    - { name: tmp, emptyDir: {} }

# 7) Distroless for compiled languages
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags='-s -w' -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
USER nonroot
ENTRYPOINT ['/server']

# Final image ~10 MB; no shell to exploit; no package manager to escalate.

# 8) Health checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
    CMD wget -qO- http://localhost:3000/healthz || exit 1

# Orchestrators (Kubernetes, Compose, ECS) use this signal.

# 9) Labels — metadata for auditing
LABEL org.opencontainers.image.title='My API'
LABEL org.opencontainers.image.source='https://github.com/me/my-api'
LABEL org.opencontainers.image.version='1.2.3'
LABEL org.opencontainers.image.created='2024-01-15T00:00:00Z'
LABEL org.opencontainers.image.revision='abc1234'
LABEL org.opencontainers.image.licenses='MIT'

# 10) BuildKit secrets (don't leak in layers)
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

# Secret never lands in the final image OR build cache.

# 11) Build cache mounts — faster repeats
# syntax=docker/dockerfile:1.7
RUN --mount=type=cache,target=/root/.npm npm ci
RUN --mount=type=cache,target=/var/cache/apt apt-get install -y curl

# 12) Multi-arch builds
docker buildx create --name multi --use
docker buildx build --platform linux/amd64,linux/arm64 -t me/myapp:1.0 --push .

# 13) Scan + sign
# • Trivy / Grype scans → fail the build on CRITICAL/HIGH
# • Cosign signs → consumers verify
cosign sign --key cosign.key me/myapp:1.0
cosign verify --key cosign.pub me/myapp:1.0

# 14) SBOM generation
syft me/myapp:1.0 -o cyclonedx-json > sbom.json
# Or anchore/sbom-action in GitHub Actions.

# 15) Image size targets (ballpark)
# • Static Go / Rust:         10-30 MB
# • Distroless Java:          150-200 MB
# • Alpine Node:              100-150 MB
# • Slim Node:                150-250 MB
# • Full Debian:              500+ MB — usually too big for prod

# Use 'docker history myapp:1.0' to find the fat layer.

# 16) Production checklist
# ✓ Non-root user
# ✓ Multi-stage build; runtime stage minimal
# ✓ Pinned base by digest
# ✓ No secrets baked in (BuildKit secrets only)
# ✓ .dockerignore excludes node_modules, .git, .env
# ✓ HEALTHCHECK present
# ✓ OCI labels for auditing
# ✓ SBOM generated + Cosign-signed
# ✓ Trivy scan in CI; fail on HIGH/CRITICAL
# ✓ Compressed with --compress where appropriate

# 17) Common bugs
# • Forgot .dockerignore → 5 GB build context; 'COPY . .' takes minutes
# • COPY before npm ci → cache busts on every source change
# • Running as root → escapes; always USER non-root
# • Hardcoded secrets in ENV → visible via docker history
# • Pinning :latest in production → unexpected rolls; pin by digest
# • Alpine with native deps (sharp, bcrypt) → segfaults; use debian-slim
# • Build args used for secrets → visible in image history; use BuildKit secrets
# • No HEALTHCHECK → orchestrator can't tell if app is ready
# • Heavy image with everything → CVE surface; cut layers per concern
# • Building each image from a different base → fragmented patching

Why it matters

Production images are multi-stage, minimal, pinned by digest, non-root, health-checked, scanned in CI, and signed. Lean on distroless or scratch for compiled languages, alpine or debian-slim for interpreted ones. Generate SBOMs per build, mount secrets via BuildKit (never bake them in), and use cache mounts so reproducible builds stay fast.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
# Pin versions, use -alpine for size, .dockerignore your node_modules,
# run as a non-root user, multi-stage for builds.
Try it Yourself »

Discussion

Loading…