Files
michaelschiemer/docker-compose.staging.yml

486 lines
18 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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