Running Magento 2 in production with Docker Compose requires more than just copying the development setup. Secrets management, health checks, zero-downtime deployment, and log aggregation are the gaps between “works on my machine” and “runs reliably in production”. I show a complete production-grade docker-compose configuration with all these pieces in place.
Production stack overview
# docker-compose.prod.yml - services overview services: nginx: # web server, SSL termination php-fpm: # PHP 8.4 FPM db: # MariaDB 10.6 redis-cache: # object cache redis-fpc: # full page cache redis-session:# session storage opensearch: # search engine (Magento 2.4.8+) varnish: # reverse proxy cache cron: # Magento cron (separate container) queue: # queue consumer
Secrets management – never put credentials in compose files
# Use Docker secrets (swarm) or .env files with restricted permissions
# .env.prod - NOT in version control, permissions 600
# docker-compose.prod.yml
services:
db:
image: mariadb:10.6
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: magento
MYSQL_USER: magento
MYSQL_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_root_password
- db_password
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
secrets:
db_root_password:
file: ./secrets/db_root_password.txt
db_password:
file: ./secrets/db_password.txt
PHP-FPM with health check
php-fpm:
build:
context: .
dockerfile: docker/php/Dockerfile.prod
args:
PHP_VERSION: "8.4"
environment:
MAGENTO_MODE: production
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
volumes:
- app_code:/var/www/html:ro # read-only in production
- magento_var:/var/www/html/var
- magento_pub_media:/var/www/html/pub/media
healthcheck:
test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"]
interval: 15s
timeout: 5s
retries: 3
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
depends_on:
db:
condition: service_healthy
opensearch:
condition: service_healthy
nginx with SSL
nginx:
image: nginx:1.26-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/prod.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/ssl:/etc/nginx/ssl:ro # cert and key
- app_code:/var/www/html:ro
- magento_pub_media:/var/www/html/pub/media:ro
- magento_pub_static:/var/www/html/pub/static:ro
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
depends_on:
php-fpm:
condition: service_healthy
Zero-downtime deployment script
#!/bin/bash
# deploy.sh - zero-downtime deployment
set -euo pipefail
COMPOSE="docker compose -f docker-compose.prod.yml"
APP_CONTAINER="magento_php-fpm_1"
echo "=== Starting deployment $(date) ==="
# 1. Pull new code (using volume or bind mount approach)
git pull origin main
# 2. Install dependencies in temp container
$COMPOSE run --rm php-fpm composer install \
--no-dev --optimize-autoloader --no-interaction
# 3. Enable maintenance mode
$COMPOSE exec php-fpm bin/magento maintenance:enable
# 4. Run database upgrades
$COMPOSE exec php-fpm bin/magento setup:upgrade --keep-generated
# 5. Recompile DI
$COMPOSE exec php-fpm bin/magento setup:di:compile
# 6. Deploy static content
$COMPOSE exec php-fpm bin/magento setup:static-content:deploy \
pl_PL en_US -f --jobs=4
# 7. Flush cache
$COMPOSE exec php-fpm bin/magento cache:flush
# 8. Disable maintenance
$COMPOSE exec php-fpm bin/magento maintenance:disable
# 9. Graceful PHP-FPM reload (no dropped requests)
$COMPOSE exec php-fpm kill -USR2 1
echo "=== Deployment complete $(date) ==="
Cron and queue consumer as separate containers
cron:
build:
context: .
dockerfile: docker/php/Dockerfile.prod
command: ["bash", "-c", "while true; do php /var/www/html/bin/magento cron:run; sleep 60; done"]
environment:
MAGENTO_MODE: production
volumes:
- app_code:/var/www/html:ro
- magento_var:/var/www/html/var
depends_on:
php-fpm:
condition: service_healthy
restart: unless-stopped
queue-consumer:
build:
context: .
dockerfile: docker/php/Dockerfile.prod
command: >
php /var/www/html/bin/magento
queue:consumers:start async.operations.all
--max-messages=10000
environment:
MAGENTO_MODE: production
volumes:
- app_code:/var/www/html:ro
- magento_var:/var/www/html/var
restart: unless-stopped
depends_on:
php-fpm:
condition: service_healthy
Log aggregation
# Log driver - send all container logs to a centralised log service
# Example: using the json-file driver with rotation, or fluentd
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
tag: "{{.Name}}/{{.ID}}"
services:
php-fpm:
logging: *default-logging
# ... other config
nginx:
logging: *default-logging
# nginx logs also go to volumes for access log analysis
volumes:
- nginx_logs:/var/log/nginx
Summary
Production Docker Compose for Magento 2 has three non-negotiable elements beyond the basic service definitions: secrets management (never credentials in compose files), health checks (so dependent services wait for real readiness), and zero-downtime deployment (maintenance mode + cache flush + graceful FPM reload). Separate containers for cron and queue consumers give independent restart policies and resource limits, preventing a runaway import from killing the web server.
