Docker Containerization
You are containerizing or reviewing the Docker setup for: $ARGUMENTS
Containerization is not just "making it run in Docker." A production container must be minimal, secure, reproducible, and observable. Apply every rule below.
Principles
- Minimal surface area. Every layer you add is an attack surface. The final image should contain only what the application needs to run.
- Reproducibility. The same
docker buildon any machine at any time produces the same image. Noapt-getpulling latest packages, no dynamic downloads. - Security by default. Non-root user, read-only filesystem, dropped capabilities — in the Dockerfile, not added later.
- Observable. Health check endpoints built in. Logs to stdout/stderr. Structured where possible.
1. Multi-Stage Dockerfile
Use multi-stage builds to keep build tools out of the production image:
# ─── Stage 1: Build ──────────────────────────────────────────────
FROM node:22.3.0-alpine3.20 AS builder
WORKDIR /app
# Copy dependency manifests first (maximise layer cache)
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# ─── Stage 2: Production ─────────────────────────────────────────
FROM node:22.3.0-alpine3.20
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Copy only production artifacts from builder
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
USER appuser
EXPOSE 8080
# Health check built into the image
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/healthz || exit 1
CMD ["node", "dist/server.js"]
Language-specific starting points:
# Python (FastAPI / Flask)
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.12-slim
RUN useradd -m appuser
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser . .
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
# PHP (Laravel / Symfony)
FROM composer:2.7 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
FROM php:8.3-fpm-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /var/www
COPY --from=composer --chown=appuser:appgroup /app/vendor ./vendor
COPY --chown=appuser:appgroup . .
USER appuser
2. Security Hardening
Non-Root User
# Alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Debian/Ubuntu
RUN useradd --uid 1001 --gid 1001 --no-create-home appuser
USER appuser
Read-Only Root Filesystem (Kubernetes)
# In your pod spec — makes the container's filesystem immutable
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
# Mount writable paths explicitly
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Image Scanning
# Scan before push
trivy image myapp:1.0.0
# Fix CRITICALs — usually means updating the base image tag
# Check what the alpine version fixes: https://pkg.alpinelinux.org/packages
# Scan in CI (fail build on CRITICAL)
trivy image --exit-code 1 --severity CRITICAL myapp:1.0.0
3. Layer Cache Optimisation
# BAD: copies everything first — any source change invalidates npm install cache
COPY . .
RUN npm ci
# GOOD: copy manifests first — npm install only re-runs if package.json changes
COPY package*.json ./
RUN npm ci
COPY . .
Keep layers that change rarely (OS packages, system deps) near the top, and layers that change often (application code) near the bottom.
4. docker-compose for Local Development
# docker-compose.yml
services:
app:
build:
context: .
target: builder # use the build stage for hot-reload
ports:
- "8080:8080"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://user:pass@db:5432/myapp
volumes:
- .:/app # mount source for hot reload
- /app/node_modules # exclude node_modules from mount
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
5. .dockerignore
Always include a .dockerignore to keep the build context small and secrets out of images:
.git
.env*
node_modules
*.log
dist
coverage
.DS_Store
*.test.*
docs
README.md
6. Checklist
- [ ] Multi-stage build — no build tools in production image
- [ ] Base image pinned to a specific version (not
latest) - [ ] Non-root user set
- [ ]
.dockerignoreexcludes dev files, secrets,.git - [ ]
HEALTHCHECKinstruction defined - [ ] No secrets in
ENV,ARG, or layers - [ ] Image scanned with Trivy — no CRITICAL CVEs
- [ ] Layer order is cache-optimised (deps before source)
- [ ]
docker-compose.ymlprovided for local development - [ ] Logs go to stdout/stderr (not files inside container)