Docker Best Practices

This guide covers best practices for building, running, and managing Docker containers in production. Following these practices will help you create secure, efficient, and maintainable Docker applications.

Dockerfile Best Practices

1. Use Specific Image Tags

❌ Bad:
FROM node:latest
FROM ubuntu:latest

Using :latest can lead to unpredictable builds

✅ Good:
FROM node:18-alpine
FROM ubuntu:22.04

Use specific version tags for reproducibility

2. Use Multi-stage Builds

✅ Benefit: Reduces final image size significantly by excluding build tools and dependencies.
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]

3. Optimize Layer Caching

❌ Bad Order:
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]

npm install runs on every code change

✅ Good Order:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "index.js"]

Dependencies cached if package.json unchanged

4. Use .dockerignore

Create a .dockerignore file to exclude unnecessary files:

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.nyc_output
coverage
*.md
.DS_Store
.vscode
.idea
*.log
dist
build
.cache
tmp
temp
💡 Tip: Similar to .gitignore, this reduces build context size and build time.

5. Minimize Image Layers

❌ Too Many Layers:
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get install -y git
RUN apt-get clean
✅ Combined Layers:
RUN apt-get update && \
    apt-get install -y curl wget git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

6. Use COPY Instead of ADD

💡 Rule: Use COPY for copying files from host. Only use ADD when you need automatic URL downloading or tar extraction.
# Good: Use COPY
COPY package.json .
COPY src/ ./src/

# Only if needed: Use ADD for URLs
ADD https://example.com/file.tar.gz /tmp/

7. Use Non-root User

# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# Set ownership
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

# Or use numeric user ID
USER 1001
✅ Security Benefit: Running as non-root reduces security risks if container is compromised.

8. Set Resource Limits

While not in Dockerfile, set limits when running containers:

# Memory limit
docker run -m 512m myapp

# CPU limit
docker run --cpus="1.5" myapp

# Both limits
docker run -m 512m --cpus="1.5" myapp

# In docker-compose.yml
services:
  web:
    deploy:
      resources:
        limits:
          cpus: '1.5'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

9. Use Health Checks

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

# Or in docker-compose.yml
services:
  web:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

10. Use Explicit Ports

❌ Vague:
EXPOSE 80
✅ Explicit:
EXPOSE 8080/tcp
EXPOSE 8080/udp

Image Security Best Practices

1. Scan Images for Vulnerabilities

# Using Docker Scout
docker scout cves myapp:latest

# Using Trivy
trivy image myapp:latest

# Using Snyk
snyk container test myapp:latest
💡 Tip: Integrate image scanning into your CI/CD pipeline before deployment.

2. Keep Base Images Updated

# Regularly update base images
FROM node:18-alpine  # Check for updates regularly
FROM ubuntu:22.04    # Use latest LTS versions

3. Minimize Attack Surface

✅ Use Minimal Base Images:
  • Alpine Linux (5MB) - for most applications
  • Distroless images - for compiled applications
  • Scratch - for statically compiled binaries

4. Don't Store Secrets in Images

⚠️ Never:
# ❌ BAD: Never do this
ENV DB_PASSWORD=secret123
RUN echo "password=secret123" > /app/config
✅ Good: Use Secrets Management
# Use Docker secrets or environment variables
docker run -e DB_PASSWORD=${DB_PASSWORD} myapp

# Or use docker-compose secrets
services:
  web:
    secrets:
      - db_password
secrets:
  db_password:
    external: true

Container Runtime Best Practices

1. Use Read-only Root Filesystem

docker run --read-only myapp

# Or in docker-compose.yml
services:
  web:
    read_only: true
    tmpfs:
      - /tmp
      - /var/cache

2. Set Security Options

# Disable new privileges
docker run --security-opt no-new-privileges:true myapp

# Use seccomp profile
docker run --security-opt seccomp=default.json myapp

# Drop capabilities
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp

3. Limit Container Access

# Don't run privileged
docker run --privileged myapp  # ❌ Bad

# Use user namespaces
docker run --userns=host myapp  # ✅ Better

# Disable network access if not needed
docker run --network=none myapp

4. Use Resource Constraints

# Always set limits
docker run \
  --memory="512m" \
  --cpus="1.0" \
  --pids-limit=100 \
  myapp

Docker Compose Best Practices

1. Use Version Control

✅ Best Practices:
  • Pin image versions (avoid :latest)
  • Use environment variables for configuration
  • Separate development and production configs

2. Use Named Volumes

version: '3.8'

services:
  db:
    volumes:
      - db-data:/var/lib/postgresql/data  # ✅ Named volume

volumes:
  db-data:
    driver: local

3. Set Restart Policies

services:
  web:
    restart: unless-stopped  # ✅ Good for production
    # restart: always         # Restarts even on failure
    # restart: on-failure     # Only on failure
    # restart: no             # Never restart

4. Use Health Checks

services:
  web:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    depends_on:
      web:
        condition: service_healthy

5. Network Isolation

version: '3.8'

services:
  web:
    networks:
      - frontend
  
  api:
    networks:
      - frontend
      - backend
  
  db:
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

Performance Optimization

1. Use Build Cache

# Build with cache
docker build -t myapp .

# Build without cache (if needed)
docker build --no-cache -t myapp .

# Use BuildKit for better caching
DOCKER_BUILDKIT=1 docker build -t myapp .

2. Parallel Builds

# Use BuildKit for parallel builds
DOCKER_BUILDKIT=1 docker build .

# Build multiple stages in parallel
docker buildx build --platform linux/amd64,linux/arm64 .

3. Optimize Image Size

💡 Tips:
  • Use multi-stage builds
  • Remove unnecessary packages
  • Clean package cache after installs
  • Use .dockerignore
  • Combine RUN commands

4. Use Specific Base Images

# Smaller images
FROM node:18-alpine        # ~40MB vs ~900MB for node:18
FROM python:3.11-slim      # ~125MB vs ~900MB for python:3.11
FROM golang:1.21-alpine    # ~300MB vs ~800MB for golang:1.21

Development Best Practices

1. Use Volume Mounts for Development

# Mount source code for hot reload
docker run -v $(pwd):/app myapp

# In docker-compose.yml
services:
  web:
    volumes:
      - ./src:/app/src        # Source code
      - /app/node_modules     # Exclude node_modules

2. Use Different Configs for Dev/Prod

# docker-compose.yml (base)
version: '3.8'
services:
  web:
    build: .
    environment:
      - NODE_ENV=${NODE_ENV:-development}

# docker-compose.prod.yml (production)
version: '3.8'
services:
  web:
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

3. Keep Development Containers Lightweight

💡 Tip: Use docker-compose.override.yml for local development overrides.

Production Best Practices

1. Use Orchestration

✅ For Production:
  • Kubernetes - for large-scale deployments
  • Docker Swarm - for simple orchestration
  • Nomad - for multi-cloud deployments

2. Implement Logging Strategy

# Use log driver
docker run --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  myapp

# Or centralized logging
docker run --log-driver=syslog \
  --log-opt syslog-address=tcp://logserver:514 \
  myapp

3. Monitor Containers

💡 Monitoring Tools:
  • Prometheus + Grafana
  • Datadog
  • New Relic
  • ELK Stack (for logs)

4. Implement Backup Strategy

# Backup volumes
docker run --rm \
  -v myapp-data:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/backup.tar.gz /data

# Restore volumes
docker run --rm \
  -v myapp-data:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/backup.tar.gz -C /

5. Use Image Signing

# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Sign image
docker build -t myregistry/myapp:1.0.0 .
docker push myregistry/myapp:1.0.0

Common Mistakes to Avoid

⚠️ Common Mistakes:
  • ❌ Using :latest tag in production
  • ❌ Running containers as root
  • ❌ Storing secrets in images or environment files
  • ❌ Not setting resource limits
  • ❌ Using large base images unnecessarily
  • ❌ Not cleaning up unused images/containers
  • ❌ Not using health checks
  • ❌ Exposing unnecessary ports
  • ❌ Not updating base images regularly
  • ❌ Not scanning images for vulnerabilities

Cleanup Best Practices

Regular Cleanup Commands

# Remove stopped containers
docker container prune

# Remove unused images
docker image prune

# Remove unused volumes
docker volume prune

# Remove unused networks
docker network prune

# Remove everything unused
docker system prune

# Remove everything including volumes
docker system prune -a --volumes

# Remove images older than 24 hours
docker image prune -a --filter "until=24h"
💡 Tip: Schedule regular cleanup jobs to prevent disk space issues.

Checklist for Production

✅ Production Readiness Checklist:
  • ✅ Use specific image tags (no :latest)
  • ✅ Run containers as non-root user
  • ✅ Set resource limits (CPU, memory)
  • ✅ Implement health checks
  • ✅ Use secrets management (not in images)
  • ✅ Scan images for vulnerabilities
  • ✅ Use read-only filesystem where possible
  • ✅ Set appropriate security options
  • ✅ Implement logging strategy
  • ✅ Set up monitoring and alerting
  • ✅ Use orchestration for high availability
  • ✅ Implement backup strategy
  • ✅ Use network isolation
  • ✅ Document all configurations
  • ✅ Test disaster recovery procedures

Next Steps