Skip to main content
Docker·8 min read

Docker in Production: 15 Best Practices Senior Engineers Follow

Move beyond 'it works on my machine.' Learn multi-stage builds, security hardening, health checks, resource limits, and image optimization for production containers.

DT

DevOps Engineer & Technical Writer

Why Most Dockerfiles Are Wrong

I've reviewed hundreds of production Dockerfiles. 80% of them have the same problems: running as root, 1GB+ images, no health checks, secrets baked in, and slow builds. Here are 15 practices that separate production-grade containers from tutorial-level ones.

PRODUCTION DOCKER ARCHITECTURE DOCKERFILE Multi-Stage Build stage separation 150MB vs 1.2GB Image Layers Base (alpine) Dependencies App code (small) REGISTRY Container Registry Signed + scanned pull Production Container Health Checks Resource Limits Read-Only Filesystem Non-Root User No secrets in image ORCHESTRATOR (Kubernetes / Docker Swarm) Pulls image - enforces limits - monitors health - restarts on failure

1. Use Multi-Stage Builds

Never ship build tools in your production image.

# BAD: 1.2GB image with compilers, dev deps, source code

FROM node:20

COPY . .

RUN npm install

RUN npm run build

CMD ["node", "dist/server.js"]

# GOOD: 150MB image with only what's needed to run

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

FROM node:20-alpine AS production

WORKDIR /app

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/node_modules ./node_modules

CMD ["node", "dist/server.js"]

2. Never Run as Root

# Create a non-root user

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Change ownership of app files

COPY --chown=appuser:appgroup . .

# Switch to non-root

USER appuser

If your container gets compromised, the attacker has root access to the container (and potentially the host through vulnerabilities). Always run as non-root.

3. Use Specific Image Tags

# BAD: could change at any time

FROM node:latest

FROM python:3

# GOOD: pinned, reproducible

FROM node:20.11.1-alpine3.19

FROM python:3.12.1-slim-bookworm

latest today isn't latest tomorrow. Pin your base images.

4. Add Health Checks

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \

CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

Without health checks, your orchestrator doesn't know if the app is actually working. It only knows the process is running.

5. Set Resource Limits

Always set memory and CPU limits in your orchestrator (not Dockerfile), but design your app to respect them:

# docker-compose.yml

services:

api:

deploy:

resources:

limits:

memory: 512M

cpus: '0.5'

reservations:

memory: 256M

cpus: '0.25'

6. Order Layers by Change Frequency

Docker caches layers. Put things that change least at the top:

FROM node:20-alpine

# These rarely change — cached

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --only=production

# This changes often — not cached but only rebuilds from here

COPY src/ ./src/

RUN npm run build

7. Use .dockerignore

node_modules

.git

.env

*.log

dist

coverage

.github

Without .dockerignore, you're sending your entire repo (including .git history) to the Docker daemon. This slows builds and can leak secrets.

8. Scan for Vulnerabilities

# Scan before pushing

docker scout cves myimage:latest

# Or use Trivy

trivy image myimage:latest

Run this in CI. Block deployments if HIGH or CRITICAL vulnerabilities are found.

9. Don't Store Secrets in Images

# BAD: secret is baked into a layer forever

ENV API_KEY=sk-12345

COPY .env .

# GOOD: pass at runtime

# (no secrets in Dockerfile at all)

Even if you delete the ENV in a later layer, it's still in the image history. Use runtime environment variables or secrets managers.

10. Use COPY, Not ADD

# BAD: ADD has magic behavior (auto-extracts archives, fetches URLs)

ADD . .

# GOOD: explicit, predictable

COPY . .

Only use ADD when you specifically need tar extraction.

11. One Process Per Container

# BAD: running nginx + app in one container

CMD nginx && node server.js

# GOOD: separate containers, orchestrated together

# Container 1: node server.js

# Container 2: nginx (reverse proxy)

One process per container = independent scaling, cleaner logs, easier debugging.

12. Set Proper Signal Handling

# Use exec form (not shell form) for CMD

# GOOD: PID 1, receives signals directly

CMD ["node", "server.js"]

# BAD: runs under /bin/sh, signals don't reach your app

CMD node server.js

With shell form, SIGTERM goes to the shell, not your app. Your app never gets a chance to gracefully shut down.

13. Use Alpine or Distroless

# Full image: 950MB

FROM node:20

# Alpine: 150MB

FROM node:20-alpine

# Distroless: 120MB, no shell (most secure)

FROM gcr.io/distroless/nodejs20

Smaller image = smaller attack surface = faster pulls.

14. Label Your Images

LABEL org.opencontainers.image.source="https://github.com/myorg/myapp"

LABEL org.opencontainers.image.version="1.2.3"

LABEL org.opencontainers.image.created="2026-06-22"

Labels help with tracking, auditing, and cleanup.

15. Set a Proper ENTRYPOINT

# For CLI tools — allow passing arguments

ENTRYPOINT ["python", "manage.py"]

CMD ["runserver", "0.0.0.0:8000"]

# Usage: docker run myapp migrate (overrides CMD, keeps ENTRYPOINT)

Quick Checklist

Before pushing any image to production, verify:

  • Running as non-root user
  • Image under 300MB
  • No secrets in any layer
  • Health check defined
  • Pinned base image tag
  • .dockerignore in place
  • Vulnerability scan passes
  • Uses exec form CMD

Follow these 15 practices and your containers will be secure, fast, and production-ready.

---

Frequently Asked Questions

What is the most important Docker security best practice?

Never run containers as root. Add USER nonroot in your Dockerfile and ensure the application files are owned by that user. Running as root means a container escape gives the attacker root on the host. Also use read-only filesystem (--read-only), drop all capabilities, and scan images for vulnerabilities in CI.

How do I reduce Docker image vulnerabilities?

Use minimal base images (Alpine, distroless), keep images updated with the latest security patches, remove unnecessary packages and tools, and scan images with tools like Trivy or Snyk in your CI pipeline. Multi-stage builds help by excluding build tools from the final image, reducing the attack surface significantly.

What is the correct way to handle Docker container health checks?

Define HEALTHCHECK in your Dockerfile with a command that verifies the application is truly ready to serve traffic, not just that the process is running. Example: HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1. Orchestrators use health checks to restart unhealthy containers and route traffic only to healthy ones.

How do I manage Docker image tags in production?

Never use latest in production — it's mutable and makes rollbacks impossible. Use immutable tags based on Git SHA, semantic version, or build number (e.g., myapp:v1.2.3-abc1234). Implement an image promotion workflow where images are tagged with environment labels as they pass through staging to production.

Should I use Docker Compose in production?

Docker Compose is designed for local development and testing, not production orchestration. For production, use Kubernetes, ECS, or Docker Swarm which provide health checking, rolling updates, auto-scaling, and self-healing. Docker Compose lacks production essentials like node failure recovery, load balancing across hosts, and zero-downtime deployments.

---