# Staging Environment Override # Usage: docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml up # # This file overrides base configuration with staging-specific settings: # - Container names with "staging-" prefix # - Traefik integration for staging.michaelschiemer.de # - Git clone functionality for staging branch # - Staging-specific networks (traefik-public, staging-internal) # - Staging-specific volumes services: # PHP-FPM Application Runtime php: image: localhost:5000/framework:latest container_name: php restart: unless-stopped networks: - app-backend - app-internal environment: - TZ=Europe/Berlin - APP_ENV=staging - APP_DEBUG=${APP_DEBUG:-true} - APP_URL=https://staging.michaelschiemer.de - APP_KEY=${APP_KEY:-} # Git Repository - clones staging branch - GIT_REPOSITORY_URL=${GIT_REPOSITORY_URL:-} - GIT_BRANCH=staging - GIT_TOKEN=${GIT_TOKEN:-} - GIT_USERNAME=${GIT_USERNAME:-} - GIT_PASSWORD=${GIT_PASSWORD:-} # Database - using separate staging database - DB_HOST=postgres - DB_PORT=5432 - DB_DATABASE=${DB_DATABASE:-michaelschiemer_staging} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} # Redis - REDIS_HOST=redis - REDIS_PORT=6379 # Cache - CACHE_DRIVER=redis - CACHE_PREFIX=${CACHE_PREFIX:-staging} # Session - SESSION_DRIVER=redis - SESSION_LIFETIME=${SESSION_LIFETIME:-120} # Queue - QUEUE_DRIVER=redis - QUEUE_CONNECTION=default # Use Docker Secrets via *_FILE pattern (Framework supports this automatically) # Note: These paths will be set by the entrypoint script after copying secrets # to /var/www/html/storage/secrets/ for www-data access # The entrypoint script will copy secrets and set these paths - DB_PASSWORD_FILE=/var/www/html/storage/secrets/db_user_password - REDIS_PASSWORD_FILE=/var/www/html/storage/secrets/redis_password - APP_KEY_FILE=/var/www/html/storage/secrets/app_key volumes: - app-code:/var/www/html - app-storage:/var/www/html/storage - app-logs:/var/www/html/storage/logs - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro secrets: - db_user_password - redis_password - app_key # Override entrypoint to only start PHP-FPM (not nginx) + fix git ownership entrypoint: ["/bin/sh", "-c"] command: - | # Copy Docker Secrets to readable location for www-data # Docker Secrets are only readable by root, but PHP (www-data) needs to read them. # We copy them here as root to a location where www-data can read them. # Note: Use $$ to escape shell variables in docker-compose YAML echo "🔐 Setting up Docker Secrets for PHP access..." SECRETS_DIR="/var/www/html/storage/secrets" # Ensure we're in the right directory cd /var/www/html || exit 1 # Create secrets directory if it doesn't exist mkdir -p "$$SECRETS_DIR" chmod 750 "$$SECRETS_DIR" chown www-data:www-data "$$SECRETS_DIR" if [ -f /run/secrets/redis_password ]; then cp /run/secrets/redis_password "$$SECRETS_DIR/redis_password" 2>/dev/null || true chmod 640 "$$SECRETS_DIR/redis_password" chown www-data:www-data "$$SECRETS_DIR/redis_password" export REDIS_PASSWORD_FILE="$$SECRETS_DIR/redis_password" echo "✅ Copied redis_password to $$SECRETS_DIR/redis_password" else echo "âš ī¸ Warning: /run/secrets/redis_password not found" fi if [ -f /run/secrets/db_user_password ]; then cp /run/secrets/db_user_password "$$SECRETS_DIR/db_user_password" 2>/dev/null || true chmod 640 "$$SECRETS_DIR/db_user_password" chown www-data:www-data "$$SECRETS_DIR/db_user_password" export DB_PASSWORD_FILE="$$SECRETS_DIR/db_user_password" echo "✅ Copied db_user_password to $$SECRETS_DIR/db_user_password" else echo "âš ī¸ Warning: /run/secrets/db_user_password not found" fi if [ -f /run/secrets/app_key ]; then cp /run/secrets/app_key "$$SECRETS_DIR/app_key" 2>/dev/null || true chmod 640 "$$SECRETS_DIR/app_key" chown www-data:www-data "$$SECRETS_DIR/app_key" export APP_KEY_FILE="$$SECRETS_DIR/app_key" echo "✅ Copied app_key to $$SECRETS_DIR/app_key" else echo "âš ī¸ Warning: /run/secrets/app_key not found" fi # Fix Git ownership issue # Ensure Git treats the mounted repository as safe regardless of owner git config --global --add safe.directory /var/www/html 2>/dev/null || true git config --system --add safe.directory /var/www/html 2>/dev/null || true # Git Clone/Pull functionality if [ -n "$GIT_REPOSITORY_URL" ]; then echo "" echo "đŸ“Ĩ Cloning/Pulling code from Git repository..." GIT_BRANCH="${GIT_BRANCH:-main}" GIT_TARGET_DIR="/var/www/html" # Setup Git credentials if [ -n "$GIT_TOKEN" ]; then GIT_URL_WITH_AUTH=$(echo "$GIT_REPOSITORY_URL" | sed "s|https://|https://${GIT_TOKEN}@|") elif [ -n "$GIT_USERNAME" ] && [ -n "$GIT_PASSWORD" ]; then GIT_URL_WITH_AUTH=$(echo "$GIT_REPOSITORY_URL" | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|") else GIT_URL_WITH_AUTH="$GIT_REPOSITORY_URL" fi # Clone or pull if [ ! -d "$GIT_TARGET_DIR/.git" ]; then echo "đŸ“Ĩ Cloning repository from $GIT_REPOSITORY_URL (branch: $GIT_BRANCH)..." if [ "$(ls -A $GIT_TARGET_DIR 2>/dev/null)" ]; then find "$GIT_TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name "storage" -exec rm -rf {} \; 2>/dev/null || true fi TEMP_CLONE="${GIT_TARGET_DIR}.tmp" rm -rf "$TEMP_CLONE" 2>/dev/null || true if git -c safe.directory=/var/www/html clone --branch "$GIT_BRANCH" --depth 1 "$GIT_URL_WITH_AUTH" "$TEMP_CLONE"; then find "$GIT_TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name "storage" -exec rm -rf {} \; 2>/dev/null || true find "$TEMP_CLONE" -mindepth 1 -maxdepth 1 ! -name "." ! -name ".." -exec mv {} "$GIT_TARGET_DIR/" \; 2>/dev/null || true rm -rf "$TEMP_CLONE" 2>/dev/null || true echo "✅ Repository cloned successfully" fi else echo "🔄 Pulling latest changes from $GIT_BRANCH..." cd "$GIT_TARGET_DIR" git -c safe.directory=/var/www/html fetch origin "$GIT_BRANCH" || echo "âš ī¸ Git fetch failed" git -c safe.directory=/var/www/html reset --hard "origin/$GIT_BRANCH" || echo "âš ī¸ Git reset failed" git -c safe.directory=/var/www/html clean -fd || true fi # Install dependencies if [ -f "$GIT_TARGET_DIR/composer.json" ]; then echo "đŸ“Ļ Installing/updating Composer dependencies..." cd "$GIT_TARGET_DIR" composer install --no-dev --optimize-autoloader --no-interaction --no-scripts || echo "âš ī¸ Composer install failed" composer dump-autoload --optimize --classmap-authoritative || true fi echo "✅ Git sync completed" else echo "" echo "â„šī¸ GIT_REPOSITORY_URL not set, using code from image" fi echo "" echo "📊 Environment variables:" env | grep -E "DB_|APP_" | grep -v "PASSWORD|KEY|SECRET" || true echo "" echo "đŸ› ī¸ Adjusting filesystem permissions..." chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null || true find /var/www/html/storage /var/www/html/bootstrap/cache -type d -exec chmod 775 {} \; 2>/dev/null || true find /var/www/html/storage /var/www/html/bootstrap/cache -type f -exec chmod 664 {} \; 2>/dev/null || true # Keep PHP-FPM secure with clear_env = yes (default) # The *_FILE environment variables are passed explicitly via docker-compose environment section # PHP's DockerSecretsResolver will read the secrets from the files specified in *_FILE vars # Start PHP-FPM only (no nginx) echo "" echo "🚀 Starting PHP-FPM..." echo "REDIS_PASSWORD_FILE: ${REDIS_PASSWORD_FILE:-NOT SET}" exec php-fpm healthcheck: test: ["CMD-SHELL", "php-fpm-healthcheck || true"] interval: 30s timeout: 10s retries: 3 start_period: 40s depends_on: redis: condition: service_started # Nginx Web Server nginx: image: localhost:5000/framework:latest container_name: nginx restart: unless-stopped networks: - traefik-public - app-backend environment: - TZ=Europe/Berlin - APP_ENV=staging - APP_DEBUG=${APP_DEBUG:-true} # Git Repository - clones staging branch - GIT_REPOSITORY_URL=${GIT_REPOSITORY_URL:-} - GIT_BRANCH=staging - GIT_TOKEN=${GIT_TOKEN:-} - GIT_USERNAME=${GIT_USERNAME:-} - GIT_PASSWORD=${GIT_PASSWORD:-} volumes: - ./deployment/stacks/staging/nginx/conf.d:/etc/nginx/conf.d:ro - app-code:/var/www/html:ro - app-storage:/var/www/html/storage:ro - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro # Wait for code to be available (cloned by php container) then start nginx entrypoint: ["/bin/sh", "-c"] command: - | # Wait for code to be available in shared volume (php container clones it) GIT_TARGET_DIR="/var/www/html" echo "âŗ [staging-nginx] Waiting for code to be available in shared volume..." for i in 1 2 3 4 5 6 7 8 9 10; do if [ -d "$$GIT_TARGET_DIR/public" ]; then echo "✅ [staging-nginx] Code found in shared volume" break fi echo " [staging-nginx] Waiting... ($$i/10)" sleep 2 done # If code still not available after wait, try to copy from image as fallback if [ ! -d "$$GIT_TARGET_DIR/public" ] && [ -d "/var/www/html.orig" ]; then echo "âš ī¸ [staging-nginx] Code not found in shared volume, copying from image..." find /var/www/html.orig -mindepth 1 -maxdepth 1 ! -name "storage" -exec cp -r {} "$$GIT_TARGET_DIR/" \; 2>/dev/null || true fi # Fix nginx upstream configuration - sites-enabled/default is a symlink to sites-available/default # This is critical: nginx config uses production-php:9000 but staging uses php container for NGINX_CONF in /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default; do if [ -f "$$NGINX_CONF" ]; then echo "🔧 [staging-nginx] Fixing PHP-FPM upstream in $$NGINX_CONF..." # Replace production-php with staging php container name sed -i 's|server production-php:9000;|server php:9000;|g' "$$NGINX_CONF" || true # Replace localhost/127.0.0.1 references sed -i '/upstream php-upstream {/,/}/s|server 127.0.0.1:9000;|server php:9000;|g' "$$NGINX_CONF" || true sed -i '/upstream php-upstream {/,/}/s|server localhost:9000;|server php:9000;|g' "$$NGINX_CONF" || true # Replace any auto-generated container names (like 5aad84af7c9e_php) sed -i 's|server [a-zA-Z0-9_-]*php:9000;|server php:9000;|g' "$$NGINX_CONF" || true # Replace any direct fastcgi_pass references too sed -i 's|fastcgi_pass 127.0.0.1:9000;|fastcgi_pass php-upstream;|g' "$$NGINX_CONF" || true sed -i 's|fastcgi_pass localhost:9000;|fastcgi_pass php-upstream;|g' "$$NGINX_CONF" || true sed -i 's|fastcgi_pass production-php:9000;|fastcgi_pass php-upstream;|g' "$$NGINX_CONF" || true echo "✅ [staging-nginx] PHP-FPM upstream fixed in $$NGINX_CONF" fi done # Start nginx only (no PHP-FPM, no Git clone - php container handles that) echo "🚀 [staging-nginx] Starting nginx..." exec nginx -g "daemon off;" labels: - "traefik.enable=true" # HTTP Router for staging subdomain - "traefik.http.routers.staging.rule=Host(`staging.michaelschiemer.de`)" - "traefik.http.routers.staging.entrypoints=websecure" - "traefik.http.routers.staging.tls=true" - "traefik.http.routers.staging.tls.certresolver=letsencrypt" # Service - "traefik.http.services.staging.loadbalancer.server.port=80" # Network - "traefik.docker.network=traefik-public" healthcheck: test: ["CMD-SHELL", "curl -f http://127.0.0.1/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s depends_on: php: condition: service_started # Remove base service dependencies and build ports: [] # Redis Cache/Session/Queue Backend (separate from production) redis: image: redis:7-alpine container_name: redis restart: unless-stopped networks: - app-backend environment: - TZ=Europe/Berlin - REDIS_PASSWORD_FILE=/run/secrets/redis_password secrets: - redis_password # 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 \ --maxmemory 256mb \ --maxmemory-policy allkeys-lru \ --save 900 1 \ --save 300 10 \ --save 60 10000 \ --appendonly yes \ --appendfsync everysec \ --requirepass "$$REDIS_PASSWORD" else exec redis-server \ --bind 0.0.0.0 \ --dir /data \ --maxmemory 256mb \ --maxmemory-policy allkeys-lru \ --save 900 1 \ --save 300 10 \ --save 60 10000 \ --appendonly yes \ --appendfsync everysec fi volumes: - redis-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro # Queue Worker (Background Jobs) queue-worker: image: localhost:5000/framework:latest container_name: queue-worker restart: unless-stopped networks: - app-backend - app-internal environment: - TZ=Europe/Berlin - APP_ENV=staging - APP_DEBUG=${APP_DEBUG:-true} # Database - using separate staging database - DB_HOST=postgres - DB_PORT=5432 - DB_DATABASE=${DB_DATABASE:-michaelschiemer_staging} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} # Use Docker Secrets via *_FILE pattern (Framework supports this automatically) - DB_PASSWORD_FILE=/run/secrets/db_user_password # Redis - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD_FILE=/run/secrets/redis_password # Queue - QUEUE_DRIVER=redis - QUEUE_CONNECTION=default - QUEUE_WORKER_SLEEP=${QUEUE_WORKER_SLEEP:-3} - QUEUE_WORKER_TRIES=${QUEUE_WORKER_TRIES:-3} - QUEUE_WORKER_TIMEOUT=${QUEUE_WORKER_TIMEOUT:-60} volumes: - app-code:/var/www/html - app-storage:/var/www/html/storage - app-logs:/var/www/html/storage/logs - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro command: php console.php queue:work --queue=default --timeout=${QUEUE_WORKER_TIMEOUT:-60} 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 depends_on: php: condition: service_started redis: condition: service_started entrypoint: "" stop_grace_period: 30s secrets: - db_user_password - redis_password - app_key # Scheduler (Cron Jobs) scheduler: image: localhost:5000/framework:latest container_name: scheduler restart: unless-stopped networks: - app-backend - app-internal environment: - TZ=Europe/Berlin - APP_ENV=staging - APP_DEBUG=${APP_DEBUG:-true} # Database - using separate staging database - DB_HOST=postgres - DB_PORT=5432 - DB_DATABASE=${DB_DATABASE:-michaelschiemer_staging} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} # Use Docker Secrets via *_FILE pattern (Framework supports this automatically) - DB_PASSWORD_FILE=/run/secrets/db_user_password # Redis - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD_FILE=/run/secrets/redis_password volumes: - app-code:/var/www/html - app-storage:/var/www/html/storage - app-logs:/var/www/html/storage/logs - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro command: php console.php scheduler:run 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 depends_on: php: condition: service_started redis: condition: service_started entrypoint: "" stop_grace_period: 30s secrets: - db_user_password - redis_password - app_key # Disable base services (override from docker-compose.base.yml) web: profiles: [never] minio: profiles: [never] networks: traefik-public: external: true app-backend: driver: bridge app-internal: external: true name: app-internal volumes: app-code: name: staging-code app-storage: name: staging-storage app-logs: name: staging-logs redis-data: name: staging-redis-data # 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/staging/db_password.txt external: false redis_password: file: ./deployment/secrets/staging/redis_password.txt external: false app_key: file: ./deployment/secrets/staging/app_key.txt external: false