# 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: test: ["CMD", "php", "-v"] interval: 30s timeout: 10s 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