Some checks failed
Deploy Application / deploy (push) Has been cancelled
497 lines
16 KiB
YAML
497 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:
|
|
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
|