Docker Production Best Practices: Security, Images, and Orchestration
Introduction
Docker simplifies packaging applications with their dependencies—but production containers demand discipline: small attack surface, reproducible builds, resource limits, observability, and graceful lifecycle handling. A docker run on a laptop is not the same as ten thousand tasks on Kubernetes.
This guide covers multi-stage builds, image hardening, secrets management, health checks, logging, and patterns for Node.js and static frontends in real deployments.
Table of Contents
- Multi-Stage Builds
- Image Size and Layer Optimization
- Security Hardening
- Runtime Configuration
- Health Checks and Graceful Shutdown
- Logging and Resource Limits
- CI/CD Integration
- Best Practices
- Common Mistakes
- Interview Questions
- Summary
- References
Multi-Stage Builds
Separate build dependencies from runtime:
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Build tools and source never ship to production image.
Image Size and Layer Optimization
- Use Alpine or distroless bases when compatible
- Combine
RUNinstructions to reduce layers (balance with cache) - Order Dockerfile: dependency files before source for cache hits
- Use
.dockerignore:
node_modules
.git
.env
*.md
coverage
COPY package.json package-lock.json ./
RUN npm ci
COPY src ./src
Changing src/ does not invalidate npm ci layer.
Security Hardening
FROM node:22-alpine
RUN addgroup -g 1001 -S app && adduser -S app -u 1001 -G app
USER app
WORKDIR /app
Never run as root in production. Scan images:
docker scout cves myapp:latest
# or trivy image myapp:latest
- Pin base image tags (
node:22.11.0-alpine), notlatest - No secrets in image layers—use runtime env or secret mounts
- Read-only root filesystem where possible:
# Kubernetes snippet
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
Runtime Configuration
Inject config at runtime:
docker run -e DATABASE_URL -e LOG_LEVEL=info myapp:latest
Use 12-factor config—environment variables, not baked-in files.
For Docker Compose:
services:
api:
image: myapp:latest
env_file: .env.production
depends_on:
db:
condition: service_healthy
Never commit .env with secrets to git.
Health Checks and Graceful Shutdown
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
Kubernetes uses probes:
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 3000
App must handle SIGTERM—stop accepting, finish in-flight, close connections.
Logging and Resource Limits
Log to stdout/stderr—aggregators collect from container runtime:
// Structured JSON logs — no file rotation inside container
logger.info({ event: "request_complete", duration_ms: 42 });
Set limits:
deploy:
resources:
limits:
cpus: "1"
memory: 512M
reservations:
memory: 256M
Without limits, one container can starve the host.
CI/CD Integration
# GitHub Actions excerpt
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/org/app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Tag with git SHA for traceability. Promote immutable images—don't rebuild without version change.
Best Practices
- Multi-stage builds—minimal runtime image.
- Non-root user and pinned base images.
- Health and readiness probes aligned with app behavior.
- Resource limits on CPU and memory.
- Immutable tags in production—avoid floating
latest. - Scan on every build—CVE gates in CI.
Common Mistakes
- Copying
node_modulesfrom host into image. - Running as root—container escape impact magnified.
- Secrets in Dockerfile
ENVorARG—visible in history. - No
.dockerignore—bloated context and leaked files. - Ignoring SIGTERM—forced kill mid-request on deploy.
- One giant image for monorepo—split services per image.
Interview Questions
- What is a multi-stage Docker build?
- How do you reduce Docker image size?
- Why run containers as non-root?
- Difference between liveness and readiness probes?
- How to handle secrets in Docker/Kubernetes?
- What happens on
docker stop? - Docker vs Kubernetes—when is each enough?
- How to cache npm installs in Docker builds?
Kubernetes Deployment Manifest
Production Kubernetes deployment excerpt:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: ghcr.io/org/api:1.2.3
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1001
livenessProbe:
httpGet:
path: /health
port: 3000
readinessProbe:
httpGet:
path: /ready
port: 3000
Use PodDisruptionBudgets during node upgrades so availability stays above minimum.
Supply Chain Security
- Sign images with Cosign
- Verify signatures in admission controllers
- Use minimal base images to reduce CVE surface
- Automate base image updates with Dependabot or Renovate
Container security is layered—image hardening complements network policies and secret management.
Local Development Parity
# docker-compose.yml for dev
services:
api:
build:
context: .
target: runner
volumes:
- ./src:/app/src # only in dev with hot reload stage
environment:
NODE_ENV: development
Dev compose can mount sources; production images should never include dev volumes or tools.
Distroless and Alpine Tradeoffs
Alpine images are small but use musl libc—some native Node addons break or need rebuild. Test thoroughly if using bcrypt, sharp, or native database drivers.
Distroless images contain only your app and runtime—no shell, no package manager. Debugging requires sidecar tools or ephemeral debug containers. Security win: attackers lack sh inside container.
FROM gcr.io/distroless/nodejs22-debian12
COPY --from=builder /app/dist /app/dist
WORKDIR /app
CMD ["dist/server.js"]
Choose base image based on compatibility matrix in CI—not only image size on paper.
Networking and Service Discovery
In Docker Compose, services communicate by name:
services:
api:
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
db:
image: postgres:16
In Kubernetes, use ClusterIP services and inject URLs via env or service mesh. Avoid hardcoding container IPs—they change on every deploy.
Document internal port conventions (3000 app, 5432 postgres) in team runbooks.
Run disaster recovery drills annually—restore image from registry to fresh cluster, verify health checks pass, and measure time-to-recovery. Container images without documented run procedures become single points of failure when the one engineer who built the Dockerfile has moved on to another company or team without leaving operational notes behind for the on-call rotation.
Implement image retention policies in registries—unbounded tag accumulation increases storage cost and expands the surface of old vulnerable images accidentally redeployed during rollback panic. Keep last N SHA tags per service plus semantic version tags; garbage-collect untagged manifests weekly with automation that logs deleted digests for audit trails required by compliance teams reviewing software supply chain controls and artifact provenance documentation during annual security assessments and customer-driven vendor questionnaires that ask specifically how long container artifacts remain available for forensic reconstruction after security incidents involving potentially compromised build pipelines or dependency confusion attacks targeting package registries used during the Docker build stage of your continuous integration workflows.
Summary
Production Docker means small, scanned, non-root images with health checks, structured logging, and resource limits. Multi-stage builds drop build tooling; runtime config stays external; orchestrators handle rollout and probes. Treat containers as cattle—immutable, observable, and disposable.