Files
michaelschiemer/docker-compose.production.yml
Michael Schiemer b1e3a0025a fix(deployment): improve Redis health check with fallback strategy
Changed health check to try without password first, then with Docker Secret.
This handles both scenarios where password might not be immediately available
or where the Secret read might fail in health check context.

Changes:
- Use CMD-SHELL instead of CMD for shell expansion support
- Try 'redis-cli ping' first (no auth)
- Fallback to authenticated ping if first attempt fails
- Properly quote password from Docker Secret

This is the eleventh cumulative fix for production deployment pipeline.

Related: commit 477fe67 (initial Redis health check fix)
2025-11-04 17:28:54 +01:00

417 lines
12 KiB
YAML

# Production Environment Override
# Usage: docker-compose -f docker-compose.base.yml -f docker-compose.production.yml --env-file .env.production up -d
#
# This file overrides base configuration with production-specific settings:
# - Stricter resource limits
# - Production restart policies (always)
# - JSON logging with proper rotation
# - No host mounts (security)
# - Production PostgreSQL configuration
# - Certbot for SSL certificates
# - Production port mappings (80, 443 for Let's Encrypt)
services:
web:
# Use pre-built image from registry (override build from base)
image: git.michaelschiemer.de:5000/framework:latest
build: null # Explicitly remove build section from base
# Production restart policy
restart: always
# Production port mappings for Let's Encrypt
ports:
- "80:80" # HTTP for ACME challenge
- "443:443" # HTTPS for production traffic
# Override volumes - use Let's Encrypt certificates and mount application code
volumes:
- certbot-conf:/etc/letsencrypt:ro
- certbot-www:/var/www/certbot:ro
# Application code via rsync deployment
- /home/deploy/michaelschiemer/current:/var/www/html:ro
environment:
- APP_ENV=production
- APP_DEBUG=false
# Security hardening
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_OVERRIDE
- NET_BIND_SERVICE # Required for binding to ports 80/443
# Stricter health checks for production
healthcheck:
test: ["CMD", "curl", "-f", "https://localhost/health"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
# JSON logging with rotation
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
compress: "true"
labels: "service,environment"
# Production resource limits (Nginx is lightweight)
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.5'
depends_on:
php:
condition: service_healthy
certbot:
condition: service_started
php:
# Use pre-built image from registry (override build from base)
image: git.michaelschiemer.de:5000/framework:latest
build: null # Explicitly remove build section from base
# Production restart policy
restart: always
# Override user setting - container must start as root for gosu to work
# The entrypoint script will use gosu to switch to appuser after setup
user: "root"
# Security hardening (applied after gosu switches to appuser)
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_OVERRIDE
environment:
- APP_ENV=production
- APP_DEBUG=false
- PHP_MEMORY_LIMIT=512M
- PHP_MAX_EXECUTION_TIME=30
# Disable Xdebug in production
- XDEBUG_MODE=off
# Use Docker Secrets via *_FILE pattern (Framework supports this automatically)
- DB_PASSWORD_FILE=/run/secrets/db_user_password
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
- APP_KEY_FILE=/run/secrets/app_key
- VAULT_ENCRYPTION_KEY_FILE=/run/secrets/vault_encryption_key
secrets:
- db_user_password
- redis_password
- app_key
- vault_encryption_key
# Stricter health checks
healthcheck:
test: ["CMD", "php", "-v"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
# JSON logging
logging:
driver: json-file
options:
max-size: "10m"
max-file: "10"
compress: "true"
labels: "service,environment"
# Production resource limits
deploy:
resources:
limits:
memory: 1G
cpus: '2.0'
reservations:
memory: 512M
cpus: '1.0'
# Production volumes
volumes:
# Mount application code from rsync deployment (read-write for storage/var directories)
- /home/deploy/michaelschiemer/current:/var/www/html:rw
# Database service removed - using external PostgreSQL Stack (deployment/stacks/postgresql/)
# Connection via app-internal network using docker-compose.postgres-override.yml
redis:
# Production restart policy
restart: always
# Use Docker Secrets for Redis password
environment:
REDIS_PASSWORD_FILE: /run/secrets/redis_password
secrets:
- redis_password
# Security hardening
security_opt:
- no-new-privileges:true
# Don't set user here - we need root to read Docker Secrets in entrypoint
# Redis will run as root, but this is acceptable for this use case
cap_drop:
- ALL
# Use entrypoint script to inject password from Docker Secret into config
# Note: Script runs as root to read Docker Secrets, then starts Redis
entrypoint: ["/bin/sh", "-c"]
command:
- |
# Read password from Docker Secret (as root)
REDIS_PASSWORD=$$(cat /run/secrets/redis_password 2>/dev/null || echo '')
# Start Redis with all settings as command line arguments (no config file to avoid conflicts)
if [ -n "$$REDIS_PASSWORD" ]; then
exec redis-server \
--bind 0.0.0.0 \
--dir /data \
--save 900 1 \
--save 300 10 \
--save 60 10000 \
--appendonly yes \
--requirepass "$$REDIS_PASSWORD"
else
exec redis-server \
--bind 0.0.0.0 \
--dir /data \
--save 900 1 \
--save 300 10 \
--save 60 10000 \
--appendonly yes
fi
# Production resource limits
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.5'
# Stricter health checks
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep -q PONG || redis-cli -a \"$$(cat /run/secrets/redis_password 2>/dev/null)\" ping | grep -q PONG"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
# JSON logging
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
compress: "true"
labels: "service,environment"
queue-worker:
# Use pre-built image from registry (override build from base)
image: git.michaelschiemer.de:5000/framework:latest
build: null # Explicitly remove build section from base
# Production restart policy
restart: always
# Override user setting - container must start as root for gosu to work
# The entrypoint script will use gosu to switch to appuser after setup
user: "root"
# No entrypoint override - queue-worker runs worker.php directly
# Worker command - direct PHP execution
command: ["php", "/var/www/html/worker.php"]
# Production volumes
volumes:
# Mount application code from rsync deployment (read-write for storage/var directories)
- /home/deploy/michaelschiemer/current:/var/www/html:rw
environment:
- APP_ENV=production
- WORKER_DEBUG=false
- WORKER_SLEEP_TIME=100000
- WORKER_MAX_JOBS=10000
# Use Docker Secrets via *_FILE pattern (Framework supports this automatically)
- DB_PASSWORD_FILE=/run/secrets/db_user_password
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
- APP_KEY_FILE=/run/secrets/app_key
- VAULT_ENCRYPTION_KEY_FILE=/run/secrets/vault_encryption_key
secrets:
- db_user_password
- redis_password
- app_key
- vault_encryption_key
# Production resource limits
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
reservations:
memory: 1G
cpus: '1.0'
# Note: replicas removed due to conflict with container_name
# To scale queue workers, use separate docker-compose service definitions
# JSON logging
logging:
driver: json-file
options:
max-size: "20m"
max-file: "10"
compress: "true"
labels: "service,environment"
# Graceful shutdown for long-running jobs
stop_grace_period: 60s
# Wait for dependencies to be healthy before starting
depends_on:
redis:
condition: service_healthy
php:
condition: service_healthy
# Note: PostgreSQL (postgres) is external service, connection via app-internal network
# Scheduler (Cron Jobs)
scheduler:
# Use same build as php service (has application code copied)
image: git.michaelschiemer.de:5000/framework:latest
container_name: scheduler
# Production restart policy
restart: always
# Override user setting - container must start as root for gosu to work
# The entrypoint script will use gosu to switch to appuser after setup
user: "root"
# Scheduler command - direct PHP execution
command: php console.php scheduler:run
# Production volumes
volumes:
# Mount application code from rsync deployment (read-write for storage/var directories)
- /home/deploy/michaelschiemer/current:/var/www/html:rw
environment:
- TZ=Europe/Berlin
- APP_ENV=production
- APP_DEBUG=false
# Use Docker Secrets via *_FILE pattern (Framework supports this automatically)
- DB_PASSWORD_FILE=/run/secrets/db_user_password
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
- APP_KEY_FILE=/run/secrets/app_key
- VAULT_ENCRYPTION_KEY_FILE=/run/secrets/vault_encryption_key
secrets:
- db_user_password
- redis_password
- app_key
- vault_encryption_key
# Production resource limits
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
# Health checks
healthcheck:
test: ["CMD-SHELL", "php -r 'exit(0);' && test -f /var/www/html/console.php || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
# JSON logging
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
compress: "true"
labels: "service,environment"
# Graceful shutdown
stop_grace_period: 30s
# Wait for dependencies to be healthy before starting
depends_on:
redis:
condition: service_healthy
php:
condition: service_healthy
# Note: PostgreSQL (postgres) is external service, connection via app-internal network
# Certbot Sidecar Container for Let's Encrypt
certbot:
image: certbot/certbot:latest
container_name: certbot
restart: always
volumes:
# Share certificates with Nginx
- certbot-conf:/etc/letsencrypt
- certbot-www:/var/www/certbot
# Logs for debugging
- certbot-logs:/var/log/letsencrypt
# Auto-renewal every 12 hours
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot --quiet; sleep 12h & wait $${!}; done;'"
networks:
- frontend
# JSON logging
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
compress: "true"
labels: "service,environment"
networks:
cache:
internal: true # Cache network is internal in production
volumes:
# Let's Encrypt SSL Certificates
certbot-conf:
driver: local
certbot-www:
driver: local
certbot-logs:
driver: local
# Application storage volume (single volume for entire storage directory)
storage:
driver: local
# Database volume removed - using external PostgreSQL Stack
# PostgreSQL data is managed by deployment/stacks/postgresql/