Back to Blog
DevOps October 7, 2025

Docker & Docker Compose for Production: A Complete Guide

Learn how to deploy production-ready applications using Docker and Docker Compose with best practices for multi-stage builds, image optimization, reverse proxies, and serving images from your project.

By Phothor Team
Docker & Docker Compose for Production: A Complete Guide
docker docker-compose devops deployment nginx production

Deploying applications to production can be complex, but Docker and Docker Compose simplify the process significantly. In this comprehensive guide, we’ll explore production-ready Docker deployments with real-world examples and best practices.

Why Docker for Production?

Docker provides several key advantages for production environments:

  • Consistency: Your application runs the same way across development, staging, and production
  • Isolation: Each service runs in its own container with dedicated resources
  • Scalability: Easy to scale horizontally by running multiple container instances
  • Portability: Deploy to any platform that supports Docker
  • Resource Efficiency: Containers share the host OS kernel, using fewer resources than VMs

Multi-Stage Docker Builds

Multi-stage builds are essential for production. They allow you to create optimized images by separating build dependencies from runtime dependencies.

Example: Node.js Application

# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./

ENV NODE_ENV=production

EXPOSE 3000

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

Example: Laravel Application

# Stage 1: Composer Dependencies
FROM composer:2 AS composer

WORKDIR /app

COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist

# Stage 2: Production
FROM php:8.2-fpm-alpine

WORKDIR /var/www/html

RUN apk add --no-cache \
    libpng-dev \
    libjpeg-turbo-dev \
    freetype-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install pdo pdo_mysql gd opcache

COPY --from=composer /app/vendor ./vendor
COPY . .

RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html/storage

EXPOSE 9000

CMD ["php-fpm"]

Docker Compose for Production

Docker Compose orchestrates multiple containers, making it perfect for applications with databases, caches, and other services.

Production Docker Compose Example

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
      - app_static:/var/www/html/public:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - frontend
      - backend

  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - DB_HOST=db
      - DB_DATABASE=${DB_DATABASE}
      - DB_USERNAME=${DB_USERNAME}
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_HOST=redis
    volumes:
      - app_static:/var/www/html/public
    depends_on:
      - db
      - redis
    restart: unless-stopped
    networks:
      - backend

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=${DB_DATABASE}
      - POSTGRES_USER=${DB_USERNAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    networks:
      - backend

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - backend

volumes:
  postgres_data:
  redis_data:
  app_static:

networks:
  frontend:
  backend:

Nginx as Reverse Proxy

Nginx acts as a reverse proxy, handling SSL termination, load balancing, and serving static files.

nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 20M;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml+rss
               application/rss+xml font/truetype font/opentype
               application/vnd.ms-fontobject image/svg+xml;

    include /etc/nginx/conf.d/*.conf;
}

Application Configuration (conf.d/app.conf)

upstream app_backend {
    server app:9000;
}

server {
    listen 80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    root /var/www/html/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app_backend;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location ~ /\. {
        deny all;
    }
}

Serving Images from Your Project

When serving images and static assets, it’s crucial to optimize delivery:

1. Volume Mounting for Static Assets

services:
  nginx:
    volumes:
      - ./public/images:/var/www/html/public/images:ro
      - ./public/uploads:/var/www/html/public/uploads:ro

2. Nginx Image Optimization

location ~* \.(jpg|jpeg|png|gif|webp)$ {
    root /var/www/html/public;

    # Enable caching
    expires 1y;
    add_header Cache-Control "public, immutable";

    # Enable compression
    gzip_static on;

    # Security headers
    add_header X-Content-Type-Options "nosniff";

    # Try webp first if browser supports it
    set $webp_suffix "";
    if ($http_accept ~* "webp") {
        set $webp_suffix ".webp";
    }

    try_files $uri$webp_suffix $uri =404;
}

location /uploads/ {
    alias /var/www/html/public/uploads/;

    # Prevent PHP execution in uploads
    location ~ \.php$ {
        deny all;
    }

    # Cache uploaded images
    expires 30d;
    add_header Cache-Control "public, no-transform";
}

3. Using CDN with Docker

For production, consider using a CDN for static assets:

const imageUrl = process.env.CDN_URL
  ? `${process.env.CDN_URL}/images/${filename}`
  : `/images/${filename}`;

Or in Laravel:

$imageUrl = config('app.cdn_url')
    ? config('app.cdn_url') . '/images/' . $filename
    : asset('images/' . $filename);

Production Best Practices

1. Environment Variables

Never hard-code secrets. Use environment files:

# .env (not committed to git)
DB_DATABASE=myapp_prod
DB_USERNAME=myapp_user
DB_PASSWORD=strong_random_password_here
REDIS_PASSWORD=another_strong_password

Load in docker-compose.yml:

services:
  app:
    env_file:
      - .env

2. Health Checks

Add health checks to ensure containers are running properly:

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

3. Resource Limits

Set resource limits to prevent containers from consuming too many resources:

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M

4. Logging

Configure proper logging:

services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

5. Automated Backups

Create a backup service:

services:
  backup:
    image: postgres:15-alpine
    command: >
      bash -c "while true; do
        pg_dump -h db -U $$POSTGRES_USER $$POSTGRES_DB > /backup/backup_$$(date +%Y%m%d_%H%M%S).sql
        find /backup -name '*.sql' -mtime +7 -delete
        sleep 86400
      done"
    environment:
      - POSTGRES_USER=${DB_USERNAME}
      - POSTGRES_DB=${DB_DATABASE}
      - PGPASSWORD=${DB_PASSWORD}
    volumes:
      - ./backups:/backup
    depends_on:
      - db
    networks:
      - backend

SSL Certificates with Let’s Encrypt

Use Certbot for automatic SSL certificates:

services:
  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

Initial certificate generation:

docker-compose run --rm certbot certonly --webroot \
  --webroot-path=/var/www/certbot \
  -d example.com \
  -d www.example.com \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

Deployment Workflow

1. Build and Push Images

docker build -t myapp:v1.0.0 .
docker tag myapp:v1.0.0 registry.example.com/myapp:v1.0.0
docker push registry.example.com/myapp:v1.0.0

2. Deploy to Production

# Pull latest images
docker-compose pull

# Stop and remove old containers
docker-compose down

# Start new containers
docker-compose up -d

# Run migrations (if needed)
docker-compose exec app php artisan migrate --force

# Clear cache
docker-compose exec app php artisan optimize

3. Zero-Downtime Deployment

For zero-downtime deployments, use multiple replicas and rolling updates:

docker-compose up -d --scale app=3 --no-recreate
docker-compose up -d --scale app=3

Monitoring and Debugging

View Logs

docker-compose logs -f app
docker-compose logs -f --tail=100 nginx

Access Container Shell

docker-compose exec app sh
docker-compose exec db psql -U postgres

Monitor Resources

docker stats
docker-compose top

Conclusion

Docker and Docker Compose provide a powerful foundation for production deployments. By following these best practices:

  • Use multi-stage builds for optimized images
  • Implement proper reverse proxy configuration
  • Secure your application with SSL/TLS
  • Set up automated backups
  • Monitor your containers
  • Implement zero-downtime deployments

You’ll have a robust, scalable production environment that’s easy to maintain and deploy.

For more insights on modern development practices, check out our Projects showcasing real-world Docker deployments, or contact us for help with your infrastructure.

Further Reading


Have questions about Docker deployments? Get in touch with our team - we’d love to help!