Back to blog
DockerDevOpsContainersSecurity

Docker Best Practices for Production Deployments

Essential Docker best practices for building secure, efficient, and production-ready container images — covering multi-stage builds, image optimization, security hardening, and container runtime configuration.

October 22, 2025·Phan Minh Anh

Docker in Production: The Basics Aren't Enough

Running docker run myapp works in development. Production requires hardened images, minimal attack surfaces, and efficient resource usage.

1. Multi-Stage Builds

Separate the build environment from the runtime image:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production
RUN addgroup -g 1001 -S nodejs \
    && adduser -S nextjs -u 1001

WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules

USER nextjs
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: runtime image with no build tools, compilers, or development dependencies.

2. Run as Non-Root

Never run containers as root:

# Create a dedicated user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Set ownership
COPY --chown=appuser:appuser . .

# Switch to non-root
USER appuser

3. Use Specific Image Tags

# Bad: unpredictable, changes without warning
FROM node:latest

# Bad: still unpredictable
FROM node:20

# Good: fully reproducible
FROM node:20.15.1-alpine3.20

4. Minimize Layers and Image Size

# Bad: multiple RUN commands = multiple layers
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*

# Good: single layer, clean up in same RUN
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl wget \
    && rm -rf /var/lib/apt/lists/*

5. Use .dockerignore

# .dockerignore
node_modules
.git
.env
*.log
coverage/
.DS_Store
Dockerfile
README.md

6. Health Checks

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

7. Resource Limits in docker-compose

services:
  web:
    image: myapp:v1.0.0
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    restart: unless-stopped
    read_only: true
    security_opt:
      - no-new-privileges:true

Security Scanning

Always scan images before pushing to production:

# Trivy (recommended)
trivy image myapp:v1.0.0

# Docker Scout (built into Docker Desktop)
docker scout cves myapp:v1.0.0

Quick Wins Summary

Practice Impact
Multi-stage builds Smaller images, no build tools in prod
Non-root user Reduced blast radius if compromised
Pinned tags Reproducible, predictable builds
Health checks Self-healing with orchestrators
Resource limits Prevent noisy neighbor issues
Image scanning Catch CVEs before deployment