# Docker Registry: registry.michaelschiemer.de (HTTPS via Traefik) services: # PHP-FPM Application Runtime app: image: git.michaelschiemer.de:5000/framework:latest container_name: app restart: unless-stopped networks: - app-internal environment: - TZ=Europe/Berlin - APP_ENV=${APP_ENV:-production} - APP_DEBUG=${APP_DEBUG:-false} - APP_URL=${APP_URL:-https://michaelschiemer.de} - APP_KEY=${APP_KEY:-} # Git Repository (optional - if set, container will clone/pull code on start) - GIT_REPOSITORY_URL=${GIT_REPOSITORY_URL:-} - GIT_BRANCH=${GIT_BRANCH:-main} - GIT_TOKEN=${GIT_TOKEN:-} - GIT_USERNAME=${GIT_USERNAME:-} - GIT_PASSWORD=${GIT_PASSWORD:-} # Database - DB_HOST=${DB_HOST:-postgres} - DB_PORT=${DB_PORT:-5432} - DB_DATABASE=${DB_DATABASE} - DB_USERNAME=${DB_USERNAME} # 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 # Use Docker Secrets via *_FILE pattern (Framework supports this automatically) - REDIS_PASSWORD_FILE=/run/secrets/redis_password secrets: - db_user_password - redis_password # Cache - CACHE_DRIVER=redis - CACHE_PREFIX=${CACHE_PREFIX:-app} # Session - SESSION_DRIVER=redis - SESSION_LIFETIME=${SESSION_LIFETIME:-120} # Queue - QUEUE_DRIVER=redis - QUEUE_CONNECTION=default 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 healthcheck: test: ["CMD-SHELL", "true"] interval: 30s timeout: 10s retries: 3 start_period: 40s depends_on: redis: condition: service_started # Nginx Web Server # Uses same image as app - clones code from Git if GIT_REPOSITORY_URL is set, then runs nginx nginx: image: git.michaelschiemer.de:5000/framework:latest container_name: nginx restart: unless-stopped networks: - traefik-public - app-internal environment: - TZ=Europe/Berlin - APP_ENV=${APP_ENV:-production} - APP_DEBUG=${APP_DEBUG:-false} # Git Repository (same as app - will clone code on start) - GIT_REPOSITORY_URL=${GIT_REPOSITORY_URL:-} - GIT_BRANCH=${GIT_BRANCH:-main} - GIT_TOKEN=${GIT_TOKEN:-} - GIT_USERNAME=${GIT_USERNAME:-} - GIT_PASSWORD=${GIT_PASSWORD:-} volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - app-storage:/var/www/html/storage:ro - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro # Use custom entrypoint that ensures code is available then starts nginx only (no PHP-FPM) entrypoint: ["/bin/sh", "-c"] command: - | # Ensure code is available in /var/www/html (from image or Git) GIT_TARGET_DIR="/var/www/html" # If storage is mounted but code is missing, copy from image's original location if [ ! -d "$$GIT_TARGET_DIR/public" ] && [ -d "/var/www/html.orig" ]; then echo "?? [nginx] Copying code from image..." # Copy everything except storage (which is a volume mount) find /var/www/html.orig -mindepth 1 -maxdepth 1 ! -name "storage" -exec cp -r {} "$$GIT_TARGET_DIR/" \; 2>/dev/null || true fi if [ -n "$$GIT_REPOSITORY_URL" ]; then # Configure Git to be non-interactive export GIT_TERMINAL_PROMPT=0 export GIT_ASKPASS=echo # Determine authentication method 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 echo "⚠️ [nginx] No Git credentials provided (GIT_TOKEN or GIT_USERNAME/GIT_PASSWORD). Using image contents." GIT_URL_WITH_AUTH="" fi if [ -n "$$GIT_URL_WITH_AUTH" ] && [ ! -d "$$GIT_TARGET_DIR/.git" ]; then echo "?? [nginx] Cloning repository from $$GIT_REPOSITORY_URL (branch: $${GIT_BRANCH:-main})..." # Remove only files/dirs that are not storage (which is a volume mount) # Clone into a temporary directory first, then move contents TEMP_CLONE="$${GIT_TARGET_DIR}.tmp" rm -rf "$$TEMP_CLONE" 2>/dev/null || true if git clone --branch "$${GIT_BRANCH:-main}" --depth 1 "$$GIT_URL_WITH_AUTH" "$$TEMP_CLONE"; then # Remove only files/dirs that are not storage (which is a volume mount) find "$$GIT_TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name "storage" -exec rm -rf {} \\; 2>/dev/null || true # Move contents from temp directory to target (preserving storage) 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 "✅ [nginx] Repository cloned successfully" else echo "? Git clone failed. Using image contents." rm -rf "$$TEMP_CLONE" 2>/dev/null || true fi else echo "?? [nginx] Pulling latest changes..." cd "$$GIT_TARGET_DIR" git fetch origin "$${GIT_BRANCH:-main}" || true git reset --hard "origin/$${GIT_BRANCH:-main}" || true git clean -fd || true fi if [ -f "$$GIT_TARGET_DIR/composer.json" ]; then echo "?? [nginx] Installing dependencies..." cd "$$GIT_TARGET_DIR" composer install --no-dev --optimize-autoloader --no-interaction --no-scripts || true composer dump-autoload --optimize --classmap-authoritative || true fi echo "? [nginx] Git sync completed" else echo "?? [nginx] GIT_REPOSITORY_URL not set, using code from image" fi # Start nginx only (no PHP-FPM) echo "?? [nginx] Starting nginx..." exec nginx -g "daemon off;" labels: - "traefik.enable=true" # HTTP Router - "traefik.http.routers.app.rule=Host(`${APP_DOMAIN:-michaelschiemer.de}`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls=true" - "traefik.http.routers.app.tls.certresolver=letsencrypt" # Service - "traefik.http.services.app.loadbalancer.server.port=80" # Middleware - "traefik.http.routers.app.middlewares=default-chain@file" # 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: app: condition: service_started # Redis Cache/Session/Queue Backend redis: image: redis:7-alpine container_name: redis restart: unless-stopped networks: - app-internal environment: - TZ=Europe/Berlin secrets: - redis_password command: > sh -c "redis-server --requirepass $$(cat /run/secrets/redis_password) --maxmemory 512mb --maxmemory-policy allkeys-lru --save 900 1 --save 300 10 --save 60 10000 --appendonly yes --appendfsync everysec" volumes: - redis-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro healthcheck: test: ["CMD", "sh", "-c", "redis-cli --no-auth-warning -a $$(cat /run/secrets/redis_password) ping"] interval: 30s timeout: 10s retries: 3 start_period: 10s # Queue Worker (Background Jobs) queue-worker: image: git.michaelschiemer.de:5000/framework:latest container_name: queue-worker restart: unless-stopped networks: - app-internal environment: - TZ=Europe/Berlin - APP_ENV=${APP_ENV:-production} - APP_DEBUG=${APP_DEBUG:-false} # Database - DB_HOST=${DB_HOST:-postgres} - DB_PORT=${DB_PORT:-5432} - DB_DATABASE=${DB_DATABASE} - DB_USERNAME=${DB_USERNAME} # 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 # Use Docker Secrets via *_FILE pattern (Framework supports this automatically) - REDIS_PASSWORD_FILE=/run/secrets/redis_password secrets: - db_user_password - 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-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 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: app: condition: service_started redis: condition: service_started # Scheduler (Cron Jobs) scheduler: image: git.michaelschiemer.de:5000/framework:latest container_name: scheduler restart: unless-stopped networks: - app-internal environment: - TZ=Europe/Berlin - APP_ENV=${APP_ENV:-production} - APP_DEBUG=${APP_DEBUG:-false} # Database - DB_HOST=${DB_HOST:-postgres} - DB_PORT=${DB_PORT:-5432} - DB_DATABASE=${DB_DATABASE} - DB_USERNAME=${DB_USERNAME} # 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 # Use Docker Secrets via *_FILE pattern (Framework supports this automatically) - REDIS_PASSWORD_FILE=/run/secrets/redis_password secrets: - db_user_password - redis_password volumes: - 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: app: condition: service_started redis: condition: service_started volumes: app-code: name: app-code external: true app-storage: name: app-storage external: true app-logs: name: app-logs external: true redis-data: name: redis-data external: true secrets: db_user_password: file: ./secrets/db_user_password.txt redis_password: file: ./secrets/redis_password.txt networks: traefik-public: external: true app-internal: external: true name: app-internal