Files
michaelschiemer/docs/deployment/production-deployment-guide.md

19 KiB

Production Deployment Guide

Umfassende Anleitung für das Deployment der Custom PHP Framework Anwendung auf dem Production Server.

Inhaltsverzeichnis

  1. Architektur-Übersicht
  2. Voraussetzungen
  3. Sicherheits-Setup
  4. Docker Registry Setup
  5. Production Image Build
  6. Deployment Prozess
  7. Troubleshooting
  8. 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: 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:

# 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/stdout oder /proc/self/fd/1 nicht 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:

  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:

# 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:

  1. Nginx nicht auf Port 80 listening (nur 443)
  2. Firewall blockiert Port 8888
  3. 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
  • .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:

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: 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