# Production Deployment Guide Umfassende Anleitung für das Deployment der Custom PHP Framework Anwendung auf dem Production Server. ## Inhaltsverzeichnis 1. [Architektur-Übersicht](#architektur-übersicht) 2. [Voraussetzungen](#voraussetzungen) 3. [Sicherheits-Setup](#sicherheits-setup) 4. [Docker Registry Setup](#docker-registry-setup) 5. [Production Image Build](#production-image-build) 6. [Deployment Prozess](#deployment-prozess) 7. [Troubleshooting](#troubleshooting) 8. [Monitoring](#monitoring) --- ## Architektur-Übersicht ### Development vs Production **Development** (docker-compose.yml): - Separate Container: Nginx + PHP-FPM - Source Code via Volume Mounts - Hot-Reload für Development - Xdebug aktiviert **Production** (docker-compose.prod.yml): - Single Container: Supervisor → Nginx + PHP-FPM - Code im Image eingebacken - Minimale Volume Mounts (nur logs/uploads) - Optimiert für Performance ### Production Stack ``` ┌─────────────────────────────────────────────────┐ │ Production Server (94.16.110.151) │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐│ │ │ Web │ │ PHP │ │ Redis ││ │ │ (Supervisor│ │ │ │ Cache ││ │ │ Nginx + │ │ │ │ ││ │ │ PHP-FPM) │ │ │ │ ││ │ └────────────┘ └────────────┘ └────────────┘│ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐│ │ │ PostgreSQL │ │ Queue │ │ Watchtower ││ │ │ Database │ │ Worker │ │ Auto-Update││ │ └────────────┘ └────────────┘ └────────────┘│ │ │ │ Monitoring (VPN-only): │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐│ │ │ Prometheus │ │ Grafana │ │ Portainer ││ │ │ :9090 │ │ :3000 │ │ :9443 ││ │ └────────────┘ └────────────┘ └────────────┘│ └─────────────────────────────────────────────────┘ ▲ │ WireGuard VPN (10.8.0.0/24) │ ┌───┴────┐ │ Client │ └────────┘ ``` --- ## Voraussetzungen ### Server Requirements - **OS**: Ubuntu 22.04 LTS (oder neuer) - **RAM**: Minimum 4GB (empfohlen: 8GB+) - **CPU**: 2+ Cores - **Disk**: 50GB+ freier Speicherplatz - **Network**: Statische IP oder DNS ### Installierte Software ```bash # Docker & Docker Compose docker --version # 24.0+ docker-compose --version # 2.20+ # WireGuard (für sicheren Zugriff) wg --version # SSL Tools openssl version ``` ### Ports **Public (Firewall offen)**: - `8888`: HTTP (optional, für HTTP→HTTPS Redirect) - `8443`: HTTPS (Hauptzugang) - `51820`: WireGuard VPN (UDP) **VPN-only (über 10.8.0.1)**: - `9090`: Prometheus - `3000`: Grafana - `9443`: Portainer **Internal (nicht extern erreichbar)**: - `5432`: PostgreSQL - `6379`: Redis - `9000`: PHP-FPM --- ## Sicherheits-Setup ### 1. WireGuard VPN WireGuard bietet verschlüsselten Zugang zum Production Server für Administration und Monitoring. **Server Installation**: ```bash # Als root auf Production Server apt update apt install -y wireguard # Schlüssel generieren cd /etc/wireguard umask 077 wg genkey | tee server_private.key | wg pubkey > server_public.key # Server Config cat > /etc/wireguard/wg0.conf <<'EOF' [Interface] Address = 10.8.0.1/24 ListenPort = 51820 PrivateKey = # Client (Development Machine) [Peer] PublicKey = AllowedIPs = 10.8.0.2/32 EOF # Service starten systemctl enable wg-quick@wg0 systemctl start wg-quick@wg0 systemctl status wg-quick@wg0 ``` **Client Configuration** (`/etc/wireguard/wg0-production.conf`): ```ini [Interface] Address = 10.8.0.2/32 PrivateKey = DNS = 1.1.1.1 [Peer] PublicKey = Endpoint = 94.16.110.151:51820 AllowedIPs = 10.8.0.0/24, 94.16.110.151/32 PersistentKeepalive = 25 ``` **Client Start**: ```bash sudo wg-quick up wg0-production sudo wg show # Verify connection ping 10.8.0.1 # Test connectivity ``` ### 2. Firewall Configuration **UFW Rules** (auf Production Server): ```bash # Default policies ufw default deny incoming ufw default allow outgoing # SSH (nur von spezifischen IPs) ufw allow from to any port 22 # WireGuard ufw allow 51820/udp # HTTP/HTTPS ufw allow 8888/tcp # HTTP (optional) ufw allow 8443/tcp # HTTPS # Enable firewall ufw enable ufw status verbose ``` ### 3. SSL/TLS Zertifikate **Development/Testing** (Self-Signed): ```bash # Bereits vorhanden in ./ssl/ # - cert.pem # - key.pem ``` **Production** (Let's Encrypt empfohlen): ```bash # Mit certbot certbot certonly --standalone -d yourdomain.com # Zertifikate nach ./ssl/ kopieren ``` --- ## Docker Registry Setup ### Local Registry on Production Server Für sichere, private Image-Verwaltung läuft eine lokale Docker Registry auf dem Production Server. **Registry starten**: ```bash docker run -d \ --restart=always \ --name registry \ -p 127.0.0.1:5000:5000 \ registry:2 ``` **Verify**: ```bash curl http://localhost:5000/v2/_catalog ``` **Registry in Docker konfigurieren**: `/etc/docker/daemon.json`: ```json { "insecure-registries": ["94.16.110.151:5000"] } ``` ```bash sudo systemctl restart docker ``` --- ## Production Image Build ### Build-Prozess Das Production Image wird lokal gebaut und dann zur Registry gepusht. **1. Production Dockerfile** (`Dockerfile.production`): ```dockerfile # Multi-stage build für optimale Image-Größe FROM php:8.3-fpm-alpine AS base # System dependencies RUN apk add --no-cache \ nginx \ supervisor \ postgresql-dev \ libpq \ && docker-php-ext-install pdo pdo_pgsql # PHP Configuration COPY docker/php/php.production.ini /usr/local/etc/php/conf.d/production.ini COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini COPY docker/php/zz-docker.production.conf /usr/local/etc/php-fpm.d/zz-docker.conf # Nginx Configuration COPY docker/nginx/nginx.production.conf /etc/nginx/nginx.conf COPY docker/nginx/default.production.conf /etc/nginx/http.d/default.conf # Supervisor Configuration COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf # Application Code WORKDIR /var/www/html COPY --chown=www-data:www-data . . # Composer dependencies (Production only) RUN composer install --no-dev --optimize-autoloader --no-interaction # NPM build RUN npm ci && npm run build # Permissions RUN chown -R www-data:www-data /var/www/html/storage \ && chmod -R 775 /var/www/html/storage # Start Supervisor (manages nginx + php-fpm) CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] ``` **2. Build Command**: ```bash # Im Projekt-Root docker build \ -f Dockerfile.production \ -t 94.16.110.151:5000/framework:latest \ . ``` **3. Push to Registry**: ```bash docker push 94.16.110.151:5000/framework:latest ``` **4. Verify Push**: ```bash curl http://94.16.110.151:5000/v2/framework/tags/list ``` ### Wichtige Konfigurationsdateien #### Supervisor Configuration (`docker/supervisor/supervisord.conf`) ```ini [supervisord] nodaemon=true silent=false logfile=/dev/null logfile_maxbytes=0 pidfile=/var/run/supervisord.pid loglevel=info [program:php-fpm] command=php-fpm -F stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 autorestart=true startretries=3 [program:nginx] command=nginx -g 'daemon off;' stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 autorestart=true startretries=3 depends_on=php-fpm ``` **Wichtige Änderungen**: - `silent=false` + `logfile=/dev/null`: Supervisor loggt nach stdout/stderr statt Datei - Grund: Python's logging kann `/dev/stdout` oder `/proc/self/fd/1` nicht im append-mode öffnen #### PHP-FPM Production Config (`docker/php/zz-docker.production.conf`) ```ini [www] user = www-data group = www-data listen = 9000 listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.max_requests = 500 ``` **Wichtig**: User/Group explizit auf `www-data` setzen, da Container als root läuft. --- ## Deployment Prozess ### Docker Compose Setup **Base Configuration** (`docker-compose.yml`): - Definiert alle Services für Development - Wird **nicht** auf Production Server deployed **Production Overrides** (`docker-compose.prod.yml`): - Merged mit base config - Production-spezifische Einstellungen ### Production Override Highlights **Web Service**: ```yaml web: image: 94.16.110.151:5000/framework:latest pull_policy: always # Immer von Registry pullen, nie bauen entrypoint: [] # Entrypoint von Base-Image clearen command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] user: root # Container läuft als root, PHP-FPM workers als www-data volumes: - ./storage/logs:/var/www/html/storage/logs:rw - ./storage/uploads:/var/www/html/storage/uploads:rw - ./ssl:/var/www/ssl:ro environment: - APP_ENV=production labels: com.centurylinklabs.watchtower.enable: "true" ``` **Wichtige Overrides**: 1. `pull_policy: always`: Verhindert lokales Build, zwingt Registry-Pull 2. `entrypoint: []`: Clearen des inherited entrypoint vom Base PHP-Image 3. `command: [...]`: Expliziter Start-Command für Supervisor 4. `user: root`: Nötig für Supervisor, PHP-FPM läuft intern als www-data ### Deployment Steps **1. Files auf Server kopieren**: ```bash # Lokale Entwicklungsmaschine (via WireGuard) scp docker-compose.prod.yml deploy@94.16.110.151:/home/deploy/framework/ scp .env.production deploy@94.16.110.151:/home/deploy/framework/.env ``` **2. Auf Server: Pull und Deploy**: ```bash # SSH auf Production Server ssh deploy@94.16.110.151 # In Projekt-Verzeichnis cd /home/deploy/framework # Pull latest image docker pull 94.16.110.151:5000/framework:latest # Deploy Stack docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d # Check Status docker-compose -f docker-compose.yml -f docker-compose.prod.yml ps ``` **3. Logs überwachen**: ```bash # Alle Container docker-compose -f docker-compose.yml -f docker-compose.prod.yml logs -f # Spezifischer Container docker logs -f web docker logs -f php ``` ### Deployment Verification **Container Health Checks**: ```bash # Alle Container sollten "healthy" sein docker-compose ps # Output sollte zeigen: # web Up (healthy) # php Up (healthy) # db Up (healthy) # redis Up (healthy) ``` **Supervisor Status (im web container)**: ```bash docker exec web supervisorctl status # Output: # nginx RUNNING pid 7, uptime 0:05:23 # php-fpm RUNNING pid 8, uptime 0:05:23 ``` **Nginx & PHP-FPM Processes**: ```bash docker exec web ps aux | grep -E 'nginx|php-fpm' # Sollte zeigen: # root 1 supervisor # root 7 nginx: master # www-data nginx: worker (mehrere) # root 8 php-fpm: master # www-data php-fpm: pool www (mehrere) ``` **Application Test**: ```bash # Von lokalem Rechner (via WireGuard) curl -k -I https://94.16.110.151:8443/ # Erwartete Response: # HTTP/2 200 # server: nginx # content-type: text/html ``` --- ## Troubleshooting ### Problem 1: Supervisor Log File Permission Denied **Symptom**: ``` PermissionError: [Errno 13] Permission denied: '/var/log/supervisor/supervisord.log' ``` **Ursache**: Supervisor kann nicht in `/var/log/supervisor/` schreiben, selbst als root. **Lösung**: `supervisord.conf` ändern: ```ini silent=false logfile=/dev/null logfile_maxbytes=0 ``` **Grund**: Python's logging library kann `/dev/stdout` oder `/proc/self/fd/1` nicht im append-mode öffnen. `/dev/null` + `silent=false` macht Supervisor's logging auf stdout/stderr. ### Problem 2: EACCES Errors in Web Container **Symptom**: ``` CRIT could not write pidfile /var/run/supervisord.pid spawnerr: unknown error making dispatchers for 'nginx': EACCES ``` **Ursache**: Web container läuft nicht als root, sondern mit inherited user von base config. **Lösung**: `docker-compose.prod.yml` - `user: root` setzen: ```yaml web: user: root ``` ### Problem 3: Docker Entrypoint Override funktioniert nicht **Symptom**: Container command zeigt entrypoint prepended: ``` /usr/local/bin/docker-entrypoint.sh /usr/bin/supervisord -c ... ``` **Ursache**: Base `docker-compose.yml` hat `web` service mit separate build context. Inherited ENTRYPOINT vom Base PHP-Image wird prepended. **Lösung**: Explizit entrypoint clearen: ```yaml web: image: 94.16.110.151:5000/framework:latest pull_policy: always entrypoint: [] # WICHTIG: Entrypoint clearen command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] ``` ### Problem 4: Queue Worker restarts kontinuierlich **Symptom**: ``` docker ps # zeigt queue-worker als "Restarting" ``` **Ursache**: Base `docker-compose.yml` command sucht `/var/www/html/worker.php` das nicht existiert. **Temporary Fix**: Service deaktivieren in `docker-compose.prod.yml`: ```yaml queue-worker: deploy: replicas: 0 ``` **Proper Fix**: Richtigen Worker-Command konfigurieren: ```yaml queue-worker: command: ["php", "/var/www/html/console.php", "queue:work"] ``` ### Problem 5: HTTP Port 80 nicht erreichbar **Symptom**: `curl http://94.16.110.151:8888/` → Connection refused **Mögliche Ursachen**: 1. Nginx nicht auf Port 80 listening (nur 443) 2. Firewall blockiert Port 8888 3. Intentional (HTTPS-only Configuration) **Debug**: ```bash # Im Container checken docker exec web netstat -tlnp | grep :80 # Nginx config testen docker exec web nginx -t # Nginx config anschauen docker exec web cat /etc/nginx/http.d/default.conf ``` **Fix (falls HTTP→HTTPS Redirect gewünscht)**: In `docker/nginx/default.production.conf`: ```nginx server { listen 80; server_name _; return 301 https://$host$request_uri; } ``` --- ## Monitoring ### Prometheus **Zugang**: http://10.8.0.1:9090 (nur via WireGuard) **Konfiguration**: `monitoring/prometheus/prometheus.yml` **Scraped Targets**: - Framework Application Metrics - Container Metrics (cAdvisor) - Node Exporter (Server Metrics) ### Grafana **Zugang**: http://10.8.0.1:3000 (nur via WireGuard) **Default Login**: - User: `admin` - Password: `${GRAFANA_PASSWORD}` (aus `.env`) **Dashboards**: `monitoring/grafana/provisioning/dashboards/` ### Portainer **Zugang**: https://10.8.0.1:9443 (nur via WireGuard) **Features**: - Container Management - Stack Deployment - Log Viewing - Resource Usage ### Watchtower Auto-Update Watchtower überwacht Container mit Label `com.centurylinklabs.watchtower.enable: "true"` und updated sie automatisch bei neuen Images. **Konfiguration**: ```yaml watchtower: environment: WATCHTOWER_CLEANUP: "true" WATCHTOWER_POLL_INTERVAL: 300 # 5 Minuten WATCHTOWER_LABEL_ENABLE: "true" WATCHTOWER_NOTIFICATIONS: "shoutrrr" WATCHTOWER_NOTIFICATION_URL: "${WATCHTOWER_NOTIFICATION_URL}" ``` **Monitoren**: ```bash docker logs -f watchtower ``` --- ## Maintenance ### Image Updates **1. Lokal neues Image bauen**: ```bash docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest . docker push 94.16.110.151:5000/framework:latest ``` **2. Auf Server**: ```bash # Watchtower erkennt Update automatisch innerhalb von 5 Minuten # Oder manuell: docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d ``` ### Database Backups ```bash # Manual Backup docker exec db pg_dump -U framework_user framework_db > backup_$(date +%Y%m%d_%H%M%S).sql # Automated (via cron) 0 2 * * * /home/deploy/scripts/backup-database.sh ``` ### Log Rotation Logs in `./storage/logs/` automatisch rotieren: ```bash # /etc/logrotate.d/framework /home/deploy/framework/storage/logs/*.log { daily rotate 14 compress delaycompress notifempty missingok create 0640 www-data www-data } ``` ### SSL Certificate Renewal **Let's Encrypt** (automatisch via certbot): ```bash certbot renew --deploy-hook "docker exec web nginx -s reload" ``` --- ## Security Checklist - [ ] WireGuard VPN konfiguriert und aktiv - [ ] Firewall (UFW) konfiguriert und enabled - [ ] Nur benötigte Ports offen (8443, 51820) - [ ] Monitoring nur via VPN erreichbar (10.8.0.1:*) - [ ] SSL/TLS Zertifikate gültig - [ ] `.env` Secrets nicht in Git committed - [ ] Database Credentials rotiert - [ ] Redis Password gesetzt - [ ] Docker Registry läuft lokal (nicht public) - [ ] Container laufen mit minimal privileges - [ ] Watchtower auto-updates aktiviert - [ ] Backup-Strategie implementiert - [ ] Log monitoring aktiv --- ## Performance Tuning ### PHP-FPM `docker/php/zz-docker.production.conf`: ```ini pm.max_children = 50 # Max. gleichzeitige Requests pm.start_servers = 10 # Initial workers pm.min_spare_servers = 5 # Min. idle workers pm.max_spare_servers = 20 # Max. idle workers pm.max_requests = 500 # Worker recycling ``` **Tuning basierend auf RAM**: - 4GB RAM: max_children = 30 - 8GB RAM: max_children = 50 - 16GB RAM: max_children = 100 ### OPcache `docker/php/opcache.ini`: ```ini opcache.enable=1 opcache.memory_consumption=128 opcache.interned_strings_buffer=8 opcache.max_accelerated_files=10000 opcache.validate_timestamps=0 # Production: keine Timestamp-Checks opcache.revalidate_freq=0 ``` ### Nginx ```nginx worker_processes auto; worker_connections 1024; keepalive_timeout 65; client_max_body_size 20M; ``` --- ## Contact & Support **Production Server**: 94.16.110.151 **VPN Gateway**: 10.8.0.1 **Documentation**: `/home/deploy/framework/docs/` **Issue Tracker**: [GitHub/GitLab URL] --- ## Change Log ### 2025-10-28 - Initial Production Deployment **Changes**: - Supervisor logging: `/dev/null` + `silent=false` - docker-compose.prod.yml: `user: root` für web, php, queue-worker - docker-compose.prod.yml: `entrypoint: []` für web service - docker-compose.prod.yml: `pull_policy: always` für registry images **Deployed**: - Image: `94.16.110.151:5000/framework:latest` - Digest: `sha256:eee1db20b9293cf611f53d01de68e94df1cfb3c748fe967849e080d19b9e4c8b` **Status**: ✅ Deployment erfolgreich, Container healthy