19 KiB
Production Deployment Guide
Umfassende Anleitung für das Deployment der Custom PHP Framework Anwendung auf dem Production Server.
Inhaltsverzeichnis
- Architektur-Übersicht
- Voraussetzungen
- Sicherheits-Setup
- Docker Registry Setup
- Production Image Build
- Deployment Prozess
- Troubleshooting
- 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
# 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: Prometheus3000: Grafana9443: Portainer
Internal (nicht extern erreichbar):
5432: PostgreSQL6379: Redis9000: PHP-FPM
Sicherheits-Setup
1. WireGuard VPN
WireGuard bietet verschlüsselten Zugang zum Production Server für Administration und Monitoring.
Server Installation:
# 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 = <server_private_key>
# Client (Development Machine)
[Peer]
PublicKey = <client_public_key>
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):
[Interface]
Address = 10.8.0.2/32
PrivateKey = <client_private_key>
DNS = 1.1.1.1
[Peer]
PublicKey = <server_public_key>
Endpoint = 94.16.110.151:51820
AllowedIPs = 10.8.0.0/24, 94.16.110.151/32
PersistentKeepalive = 25
Client Start:
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):
# Default policies
ufw default deny incoming
ufw default allow outgoing
# SSH (nur von spezifischen IPs)
ufw allow from <deine_ip> 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):
# Bereits vorhanden in ./ssl/
# - cert.pem
# - key.pem
Production (Let's Encrypt empfohlen):
# 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:
docker run -d \
--restart=always \
--name registry \
-p 127.0.0.1:5000:5000 \
registry:2
Verify:
curl http://localhost:5000/v2/_catalog
Registry in Docker konfigurieren:
/etc/docker/daemon.json:
{
"insecure-registries": ["94.16.110.151:5000"]
}
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):
# 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:
# Im Projekt-Root
docker build \
-f Dockerfile.production \
-t 94.16.110.151:5000/framework:latest \
.
3. Push to Registry:
docker push 94.16.110.151:5000/framework:latest
4. Verify Push:
curl http://94.16.110.151:5000/v2/framework/tags/list
Wichtige Konfigurationsdateien
Supervisor Configuration (docker/supervisor/supervisord.conf)
[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/stdoutoder/proc/self/fd/1nicht im append-mode öffnen
PHP-FPM Production Config (docker/php/zz-docker.production.conf)
[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:
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:
pull_policy: always: Verhindert lokales Build, zwingt Registry-Pullentrypoint: []: Clearen des inherited entrypoint vom Base PHP-Imagecommand: [...]: Expliziter Start-Command für Supervisoruser: root: Nötig für Supervisor, PHP-FPM läuft intern als www-data
Deployment Steps
1. Files auf Server kopieren:
# 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:
# 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:
# 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:
# 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):
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:
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:
# 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:
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:
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:
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:
queue-worker:
deploy:
replicas: 0
Proper Fix: Richtigen Worker-Command konfigurieren:
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:
- Nginx nicht auf Port 80 listening (nur 443)
- Firewall blockiert Port 8888
- Intentional (HTTPS-only Configuration)
Debug:
# 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:
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:
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:
docker logs -f watchtower
Maintenance
Image Updates
1. Lokal neues Image bauen:
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:
# 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
# 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:
# /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):
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
.envSecrets 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:
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:
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
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: rootfür web, php, queue-worker - docker-compose.prod.yml:
entrypoint: []für web service - docker-compose.prod.yml:
pull_policy: alwaysfür registry images
Deployed:
- Image:
94.16.110.151:5000/framework:latest - Digest:
sha256:eee1db20b9293cf611f53d01de68e94df1cfb3c748fe967849e080d19b9e4c8b
Status: ✅ Deployment erfolgreich, Container healthy