Changed APP_DEBUG from ${APP_DEBUG:-false} to hardcoded false value
in all 4 services (php, nginx, queue-worker, scheduler).
This prevents any server-side .env or environment variables from
accidentally enabling debug mode in staging, which was causing
detailed error pages to be displayed.
486 lines
18 KiB
YAML
486 lines
18 KiB
YAML
# 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=false
|
||
- 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=false
|
||
# 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=false
|
||
# 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=false
|
||
# 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
|
||
|