12 min read A. Sulaiman

Docker Best Practices for Production

Learn essential Docker best practices for building secure, efficient, and production-ready container images

Building production-ready Docker containers requires more than just getting your application to run. This guide covers essential best practices for security, performance, and maintainability.

Docker Architecture
Figure 1: Docker architecture and layering system

1. Use Official Base Images

Always start with official, minimal base images from trusted sources.

# ❌ Bad - Unknown source, bloated
FROM ubuntu:latest

# ✅ Good - Official, minimal
FROM python:3.11-slim

# ✅ Better - Even smaller
FROM python:3.11-alpine

Why This Matters

Base Image Size Attack Surface
ubuntu:latest 77 MB High
python:3.11-slim 125 MB Medium
python:3.11-alpine 49 MB Low

2. Multi-Stage Builds

Reduce final image size by separating build and runtime stages.

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

# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: Image size reduced from 1.2GB → 180MB

3. Layer Optimization

Order Dockerfile instructions to maximize cache usage.

# ✅ Optimal ordering
FROM python:3.11-slim

# 1. Install system dependencies (rarely change)
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 2. Copy dependency files (change occasionally)
COPY requirements.txt .

# 3. Install Python packages
RUN pip install --no-cache-dir -r requirements.txt

# 4. Copy application code (changes frequently)
COPY . .

# 5. Run application
CMD ["python", "app.py"]

Docker Layers
Figure 2: Docker layer caching strategy

4. Security Best Practices

Run as Non-Root User

# Create non-root user
RUN useradd -m -u 1000 appuser

# Switch to non-root user
USER appuser

# Set working directory with proper permissions
WORKDIR /home/appuser/app

Scan for Vulnerabilities

# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Scan image
trivy image myapp:latest

# Fail build on high/critical vulnerabilities
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest

Use Secret Management

# ❌ Bad - Secrets in ENV
ENV DB_PASSWORD=mysecretpass

# ✅ Good - Use Docker secrets or mounted volumes
# Secrets mounted at runtime

5. Health Checks

Add health checks for container orchestration.

# Python Flask example
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1

# Node.js example
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node healthcheck.js || exit 1
# Python health check endpoint
@app.route('/health')
def health_check():
    try:
        # Check database connection
        db.session.execute('SELECT 1')
        return jsonify({"status": "healthy"}), 200
    except Exception as e:
        return jsonify({"status": "unhealthy", "error": str(e)}), 503

6. Minimize Image Size

# Combine RUN commands
RUN apt-get update && apt-get install -y \
    package1 \
    package2 \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# Use --no-cache for package managers
RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci --only=production && npm cache clean --force

# Remove unnecessary files
RUN rm -rf /tmp/* /var/tmp/* \
    && find /var/log -type f -delete

7. Use .dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
*.md
tests/
.pytest_cache
__pycache__
*.pyc
.vscode
.idea

8. Production Dockerfile Example

# Production-ready Python application
FROM python:3.11-slim AS builder

# Install build dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-dev \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Runtime stage
FROM python:3.11-slim

# Install runtime dependencies only
RUN apt-get update && apt-get install -y \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN useradd -m -u 1000 appuser
USER appuser
WORKDIR /home/appuser/app

# Copy installed packages from builder
COPY --from=builder /root/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH

# Copy application
COPY --chown=appuser:appuser . .

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1

# Expose port
EXPOSE 5000

# Run application
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

9. Docker Compose for Development

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - ./app:/home/appuser/app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 3s
      retries: 3

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

10. CI/CD Integration

# GitHub Actions example
name: Docker Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan with Trivy
        run: |
          trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:${{ github.sha }}

      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push myapp:${{ github.sha }}

Docker CI/CD Pipeline
Figure 3: Docker build and deployment pipeline

Checklist for Production

  • [ ] Use official minimal base images
  • [ ] Implement multi-stage builds
  • [ ] Run as non-root user
  • [ ] Add health checks
  • [ ] Scan for vulnerabilities
  • [ ] Use .dockerignore
  • [ ] Optimize layer caching
  • [ ] Remove unnecessary files
  • [ ] Set resource limits
  • [ ] Implement logging strategy
  • [ ] Use semantic versioning for tags
  • [ ] Document environment variables

Common Pitfalls to Avoid

  1. Using latest tag - Always use specific versions
  2. Running as root - Security vulnerability
  3. Large images - Slow deployments and increased attack surface
  4. Secrets in images - Use secrets management
  5. No health checks - Orchestrators can't manage containers properly

Monitoring Container Health

# View container stats
docker stats

# Check container logs
docker logs -f container_name

# Inspect container
docker inspect container_name

# View resource usage
docker system df

Conclusion

Following these Docker best practices will result in:
- ✅ Smaller images (50-80% size reduction)
- ✅ Faster builds (better cache utilization)
- ✅ Better security (reduced attack surface)
- ✅ Reliable deployments (health checks and proper error handling)

Start with these fundamentals, then customize based on your specific needs.


Need help containerizing your applications? Contact us for Docker consulting.