- Add docker volume prune to deploy.sh to prevent stale code issues - Add automatic migrations and cache warmup to staging entrypoint - Fix nginx race condition by waiting for PHP-FPM before starting - Improve PHP healthcheck to use php-fpm-healthcheck - Add curl to production nginx Dockerfile for healthchecks - Add ensureSeedsTable() to SeedRepository for automatic table creation - Update SeedCommand to ensure seeds table exists before operations This prevents 502 Bad Gateway errors during deployment and ensures fresh code is deployed without volume cache issues.
498 lines
16 KiB
YAML
498 lines
16 KiB
YAML
# Production Environment Override
|
|
# Usage: docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up
|
|
#
|
|
# This file configures services for production deployment:
|
|
# - Production-specific service names (production-*)
|
|
# - Private registry images (localhost:5000/framework:latest)
|
|
# - Git-based code deployment from main branch
|
|
# - Traefik integration for michaelschiemer.de domain
|
|
# - PostgreSQL connection via postgres-production-internal network
|
|
# - Production-grade resource limits and security settings
|
|
# - Docker Secrets for sensitive configuration
|
|
|
|
services:
|
|
php:
|
|
image: localhost:5000/framework:latest
|
|
container_name: production-php
|
|
restart: unless-stopped
|
|
entrypoint:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
echo "[Production Entrypoint] Starting initialization..."
|
|
|
|
# Copy Docker Secrets to /tmp for permission workaround
|
|
if [ -f /run/secrets/db_user_password ]; then
|
|
cp /run/secrets/db_user_password /tmp/db_user_password
|
|
chmod 644 /tmp/db_user_password
|
|
export DB_PASSWORD_FILE=/tmp/db_user_password
|
|
fi
|
|
|
|
if [ -f /run/secrets/redis_password ]; then
|
|
cp /run/secrets/redis_password /tmp/redis_password
|
|
chmod 644 /tmp/redis_password
|
|
export REDIS_PASSWORD_FILE=/tmp/redis_password
|
|
fi
|
|
|
|
if [ -f /run/secrets/app_key ]; then
|
|
cp /run/secrets/app_key /tmp/app_key
|
|
chmod 644 /tmp/app_key
|
|
export APP_KEY_FILE=/tmp/app_key
|
|
fi
|
|
|
|
if [ -f /run/secrets/vault_encryption_key ]; then
|
|
cp /run/secrets/vault_encryption_key /tmp/vault_encryption_key
|
|
chmod 644 /tmp/vault_encryption_key
|
|
export VAULT_ENCRYPTION_KEY_FILE=/tmp/vault_encryption_key
|
|
fi
|
|
|
|
if [ -f /run/secrets/git_token ]; then
|
|
cp /run/secrets/git_token /tmp/git_token
|
|
chmod 644 /tmp/git_token
|
|
GIT_TOKEN=$(cat /tmp/git_token)
|
|
export GIT_TOKEN
|
|
fi
|
|
|
|
# Git deployment with authentication
|
|
if [ -n "$GIT_REPOSITORY_URL" ] && [ -n "$GIT_TOKEN" ]; then
|
|
echo "[Production Entrypoint] Configuring Git deployment..."
|
|
|
|
# Configure Git credentials
|
|
git config --global credential.helper store
|
|
echo "https://oauth2:${GIT_TOKEN}@git.michaelschiemer.de" > ~/.git-credentials
|
|
|
|
cd /var/www/html
|
|
|
|
# Clone repository if not exists
|
|
if [ ! -d .git ]; then
|
|
echo "[Production Entrypoint] Cloning repository..."
|
|
git clone --branch ${GIT_BRANCH:-main} ${GIT_REPOSITORY_URL} /tmp/repo
|
|
mv /tmp/repo/.git .
|
|
git reset --hard HEAD
|
|
else
|
|
echo "[Production Entrypoint] Pulling latest changes..."
|
|
git fetch origin ${GIT_BRANCH:-main}
|
|
git reset --hard origin/${GIT_BRANCH:-main}
|
|
fi
|
|
|
|
echo "[Production Entrypoint] Git deployment completed"
|
|
else
|
|
echo "[Production Entrypoint] Git deployment skipped (no repository configured)"
|
|
fi
|
|
|
|
# Install/Update Composer dependencies (production mode)
|
|
if [ -f composer.json ]; then
|
|
echo "[Production Entrypoint] Installing Composer dependencies (production mode)..."
|
|
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
|
|
fi
|
|
|
|
# Run database migrations
|
|
if [ -f console.php ]; then
|
|
echo "[Production Entrypoint] Running database migrations..."
|
|
php console.php db:migrate --force || echo "[Production Entrypoint] Migration failed or no migrations pending"
|
|
fi
|
|
|
|
# Warm up caches
|
|
if [ -f console.php ]; then
|
|
echo "[Production Entrypoint] Warming up caches..."
|
|
php console.php cache:warm || echo "[Production Entrypoint] Cache warm-up skipped"
|
|
fi
|
|
|
|
# Set proper permissions
|
|
chown -R www-data:www-data /var/www/html/storage /var/www/html/var || true
|
|
chmod -R 775 /var/www/html/storage /var/www/html/var || true
|
|
|
|
echo "[Production Entrypoint] Initialization complete, starting PHP-FPM..."
|
|
exec php-fpm
|
|
environment:
|
|
- APP_ENV=production
|
|
- APP_DEBUG=false
|
|
- APP_NAME=${APP_NAME:-Michael Schiemer}
|
|
- APP_TIMEZONE=${APP_TIMEZONE:-Europe/Berlin}
|
|
- APP_LOCALE=${APP_LOCALE:-de}
|
|
- APP_URL=https://michaelschiemer.de
|
|
- FORCE_HTTPS=true
|
|
- GIT_REPOSITORY_URL=${GIT_REPOSITORY_URL:-https://git.michaelschiemer.de/michael/framework.git}
|
|
- GIT_BRANCH=${GIT_BRANCH:-main}
|
|
- DB_DRIVER=pgsql
|
|
- DB_HOST=postgres
|
|
- DB_PORT=5432
|
|
- DB_DATABASE=${DB_DATABASE:-michaelschiemer}
|
|
- DB_USERNAME=${DB_USERNAME:-postgres}
|
|
- DB_PASSWORD_FILE=/tmp/db_user_password
|
|
- REDIS_HOST=production-redis
|
|
- REDIS_PORT=6379
|
|
- REDIS_PASSWORD_FILE=/tmp/redis_password
|
|
- APP_KEY_FILE=/tmp/app_key
|
|
- VAULT_ENCRYPTION_KEY_FILE=/tmp/vault_encryption_key
|
|
- OPCACHE_ENABLED=true
|
|
- ANALYTICS_ENABLED=true
|
|
- ANALYTICS_TRACK_PERFORMANCE=false
|
|
- SESSION_FINGERPRINT_STRICT=true
|
|
- ADMIN_ALLOWED_IPS=${ADMIN_ALLOWED_IPS:-127.0.0.1,::1}
|
|
- COMPOSE_PROJECT_NAME=framework-production
|
|
volumes:
|
|
- app-code:/var/www/html
|
|
- app-storage:/var/www/html/storage
|
|
- app-logs:/var/www/html/storage/logs
|
|
- composer-cache:/root/.composer/cache
|
|
secrets:
|
|
- db_user_password
|
|
- redis_password
|
|
- app_key
|
|
- vault_encryption_key
|
|
- git_token
|
|
networks:
|
|
- app-backend
|
|
- app-internal
|
|
healthcheck:
|
|
# Check if PHP-FPM is accepting connections
|
|
test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 60s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 1G
|
|
cpus: '2.0'
|
|
reservations:
|
|
memory: 512M
|
|
cpus: '1.0'
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "5"
|
|
|
|
nginx:
|
|
image: nginx:alpine
|
|
container_name: production-nginx
|
|
restart: unless-stopped
|
|
depends_on:
|
|
php:
|
|
condition: service_healthy
|
|
volumes:
|
|
- app-code:/var/www/html:ro
|
|
- ./docker/nginx/default.traefik.conf:/etc/nginx/conf.d/default.conf:ro
|
|
- app-logs:/var/www/html/storage/logs
|
|
networks:
|
|
- app-backend
|
|
- traefik-public
|
|
labels:
|
|
# Traefik Configuration
|
|
- "traefik.enable=true"
|
|
- "traefik.docker.network=traefik-public"
|
|
|
|
# Primary Domain Router (HTTPS)
|
|
- "traefik.http.routers.production.rule=Host(`michaelschiemer.de`) || Host(`www.michaelschiemer.de`)"
|
|
- "traefik.http.routers.production.entrypoints=websecure"
|
|
- "traefik.http.routers.production.tls=true"
|
|
- "traefik.http.routers.production.tls.certresolver=letsencrypt"
|
|
- "traefik.http.routers.production.service=production"
|
|
|
|
# HTTP to HTTPS Redirect
|
|
- "traefik.http.routers.production-http.rule=Host(`michaelschiemer.de`) || Host(`www.michaelschiemer.de`)"
|
|
- "traefik.http.routers.production-http.entrypoints=web"
|
|
- "traefik.http.routers.production-http.middlewares=production-redirect-https"
|
|
|
|
# Middlewares
|
|
- "traefik.http.middlewares.production-redirect-https.redirectscheme.scheme=https"
|
|
- "traefik.http.middlewares.production-redirect-https.redirectscheme.permanent=true"
|
|
|
|
# WWW to non-WWW redirect
|
|
- "traefik.http.middlewares.production-redirect-www.redirectregex.regex=^https://www\\.michaelschiemer\\.de/(.*)"
|
|
- "traefik.http.middlewares.production-redirect-www.redirectregex.replacement=https://michaelschiemer.de/$${1}"
|
|
- "traefik.http.middlewares.production-redirect-www.redirectregex.permanent=true"
|
|
|
|
# Security Headers
|
|
- "traefik.http.middlewares.production-security-headers.headers.stsSeconds=31536000"
|
|
- "traefik.http.middlewares.production-security-headers.headers.stsIncludeSubdomains=true"
|
|
- "traefik.http.middlewares.production-security-headers.headers.stsPreload=true"
|
|
- "traefik.http.middlewares.production-security-headers.headers.forceSTSHeader=true"
|
|
- "traefik.http.middlewares.production-security-headers.headers.frameDeny=true"
|
|
- "traefik.http.middlewares.production-security-headers.headers.contentTypeNosniff=true"
|
|
- "traefik.http.middlewares.production-security-headers.headers.browserXssFilter=true"
|
|
- "traefik.http.middlewares.production-security-headers.headers.referrerPolicy=strict-origin-when-cross-origin"
|
|
|
|
# Apply middleware chain
|
|
- "traefik.http.routers.production.middlewares=production-redirect-www,production-security-headers,production-rate-limit"
|
|
|
|
# Rate Limiting
|
|
- "traefik.http.middlewares.production-rate-limit.ratelimit.average=100"
|
|
- "traefik.http.middlewares.production-rate-limit.ratelimit.burst=200"
|
|
- "traefik.http.middlewares.production-rate-limit.ratelimit.period=1s"
|
|
|
|
# Service Configuration
|
|
- "traefik.http.services.production.loadbalancer.server.port=80"
|
|
- "traefik.http.services.production.loadbalancer.healthcheck.path=/health"
|
|
- "traefik.http.services.production.loadbalancer.healthcheck.interval=30s"
|
|
- "traefik.http.services.production.loadbalancer.healthcheck.timeout=5s"
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 512M
|
|
cpus: '1.0'
|
|
reservations:
|
|
memory: 256M
|
|
cpus: '0.5'
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "5"
|
|
|
|
production-redis:
|
|
image: redis:7-alpine
|
|
container_name: production-redis
|
|
restart: unless-stopped
|
|
entrypoint:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
if [ -f /run/secrets/redis_password ]; then
|
|
cp /run/secrets/redis_password /tmp/redis_password
|
|
chmod 644 /tmp/redis_password
|
|
REDIS_PASSWORD=$(cat /tmp/redis_password)
|
|
exec redis-server \
|
|
--requirepass "$REDIS_PASSWORD" \
|
|
--maxmemory 512mb \
|
|
--maxmemory-policy allkeys-lru \
|
|
--save 900 1 \
|
|
--save 300 10 \
|
|
--save 60 10000 \
|
|
--appendonly yes \
|
|
--appendfsync everysec
|
|
else
|
|
echo "ERROR: Redis password secret not found"
|
|
exit 1
|
|
fi
|
|
volumes:
|
|
- redis-data:/data
|
|
secrets:
|
|
- redis_password
|
|
networks:
|
|
- app-backend
|
|
healthcheck:
|
|
test: ["CMD", "sh", "-c", "redis-cli --no-auth-warning -a $(cat /tmp/redis_password 2>/dev/null || echo '') ping || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 512M
|
|
cpus: '1.0'
|
|
reservations:
|
|
memory: 256M
|
|
cpus: '0.5'
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
queue-worker:
|
|
image: localhost:5000/framework:latest
|
|
container_name: production-queue-worker
|
|
restart: unless-stopped
|
|
entrypoint:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
echo "[Queue Worker] Starting initialization..."
|
|
|
|
# Copy Docker Secrets
|
|
if [ -f /run/secrets/db_user_password ]; then
|
|
cp /run/secrets/db_user_password /tmp/db_user_password
|
|
chmod 644 /tmp/db_user_password
|
|
export DB_PASSWORD_FILE=/tmp/db_user_password
|
|
fi
|
|
|
|
if [ -f /run/secrets/redis_password ]; then
|
|
cp /run/secrets/redis_password /tmp/redis_password
|
|
chmod 644 /tmp/redis_password
|
|
export REDIS_PASSWORD_FILE=/tmp/redis_password
|
|
fi
|
|
|
|
echo "[Queue Worker] Starting worker process..."
|
|
exec php /var/www/html/worker.php
|
|
environment:
|
|
- APP_ENV=production
|
|
- APP_DEBUG=false
|
|
- DB_HOST=postgres
|
|
- DB_PORT=5432
|
|
- DB_DATABASE=${DB_DATABASE:-michaelschiemer}
|
|
- DB_USERNAME=${DB_USERNAME:-postgres}
|
|
- DB_PASSWORD_FILE=/tmp/db_user_password
|
|
- REDIS_HOST=production-redis
|
|
- REDIS_PORT=6379
|
|
- REDIS_PASSWORD_FILE=/tmp/redis_password
|
|
- WORKER_SLEEP_TIME=${WORKER_SLEEP_TIME:-100000}
|
|
- WORKER_MAX_JOBS=${WORKER_MAX_JOBS:-10000}
|
|
volumes:
|
|
- app-code:/var/www/html:ro
|
|
- app-storage:/var/www/html/storage
|
|
- app-logs:/var/www/html/storage/logs
|
|
secrets:
|
|
- db_user_password
|
|
- redis_password
|
|
networks:
|
|
- app-backend
|
|
- app-internal
|
|
depends_on:
|
|
php:
|
|
condition: service_healthy
|
|
production-redis:
|
|
condition: service_healthy
|
|
stop_grace_period: 30s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 1G
|
|
cpus: '1.0'
|
|
reservations:
|
|
memory: 512M
|
|
cpus: '0.5'
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
scheduler:
|
|
image: localhost:5000/framework:latest
|
|
container_name: production-scheduler
|
|
restart: unless-stopped
|
|
entrypoint:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
echo "[Scheduler] Starting initialization..."
|
|
|
|
# Copy Docker Secrets
|
|
if [ -f /run/secrets/db_user_password ]; then
|
|
cp /run/secrets/db_user_password /tmp/db_user_password
|
|
chmod 644 /tmp/db_user_password
|
|
export DB_PASSWORD_FILE=/tmp/db_user_password
|
|
fi
|
|
|
|
if [ -f /run/secrets/redis_password ]; then
|
|
cp /run/secrets/redis_password /tmp/redis_password
|
|
chmod 644 /tmp/redis_password
|
|
export REDIS_PASSWORD_FILE=/tmp/redis_password
|
|
fi
|
|
|
|
echo "[Scheduler] Starting scheduler process..."
|
|
exec php /var/www/html/scheduler.php
|
|
environment:
|
|
- APP_ENV=production
|
|
- APP_DEBUG=false
|
|
- DB_HOST=postgres
|
|
- DB_PORT=5432
|
|
- DB_DATABASE=${DB_DATABASE:-michaelschiemer}
|
|
- DB_USERNAME=${DB_USERNAME:-postgres}
|
|
- DB_PASSWORD_FILE=/tmp/db_user_password
|
|
- REDIS_HOST=production-redis
|
|
- REDIS_PORT=6379
|
|
- REDIS_PASSWORD_FILE=/tmp/redis_password
|
|
volumes:
|
|
- app-code:/var/www/html:ro
|
|
- app-storage:/var/www/html/storage
|
|
- app-logs:/var/www/html/storage/logs
|
|
secrets:
|
|
- db_user_password
|
|
- redis_password
|
|
networks:
|
|
- app-backend
|
|
- app-internal
|
|
depends_on:
|
|
php:
|
|
condition: service_healthy
|
|
production-redis:
|
|
condition: service_healthy
|
|
stop_grace_period: 30s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 512M
|
|
cpus: '0.5'
|
|
reservations:
|
|
memory: 256M
|
|
cpus: '0.25'
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# Disable base services (override from docker-compose.base.yml)
|
|
web:
|
|
profiles: [never]
|
|
|
|
php-test:
|
|
profiles: [never]
|
|
|
|
minio:
|
|
profiles: [never]
|
|
|
|
# Networks
|
|
networks:
|
|
app-backend:
|
|
driver: bridge
|
|
app-internal:
|
|
external: true
|
|
name: app-internal
|
|
traefik-public:
|
|
external: true
|
|
name: traefik-public
|
|
|
|
# Volumes
|
|
volumes:
|
|
app-code:
|
|
driver: local
|
|
|
|
app-storage:
|
|
driver: local
|
|
|
|
app-logs:
|
|
driver: local
|
|
|
|
redis-data:
|
|
driver: local
|
|
|
|
composer-cache:
|
|
driver: local
|
|
|
|
# Docker Secrets Configuration
|
|
# Secrets are inherited from docker-compose.base.yml
|
|
# But we need to explicitly define them here to ensure they're available
|
|
secrets:
|
|
db_user_password:
|
|
file: ./deployment/secrets/production/db_password.txt
|
|
external: false
|
|
redis_password:
|
|
file: ./deployment/secrets/production/redis_password.txt
|
|
external: false
|
|
app_key:
|
|
file: ./deployment/secrets/production/app_key.txt
|
|
external: false
|
|
vault_encryption_key:
|
|
file: ./deployment/secrets/production/vault_encryption_key.txt
|
|
external: false
|
|
git_token:
|
|
file: ./deployment/secrets/production/git_token.txt
|
|
external: false
|