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.
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:ro2. 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_passwordLoad in docker-compose.yml:
services:
app:
env_file:
- .env2. 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: 40s3. 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: 512M4. 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:
- backendSSL 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-emailDeployment 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.02. 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 optimize3. 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=3Monitoring and Debugging
View Logs
docker-compose logs -f app
docker-compose logs -f --tail=100 nginxAccess Container Shell
docker-compose exec app sh
docker-compose exec db psql -U postgresMonitor Resources
docker stats
docker-compose topConclusion
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
- Docker Official Documentation
- Docker Compose Documentation
- Nginx Configuration Guide
- Let’s Encrypt Documentation
Have questions about Docker deployments? Get in touch with our team - we’d love to help!