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)
417 lines
12 KiB
YAML
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/
|