- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
16 KiB
Application Stack Deployment Prozess
Übersicht
Dieses Dokument erklärt, wie genau das Deployment in den Application Stack abläuft. Es gibt zwei Deployment-Szenarien:
- Initial Deployment - Erstes Setup des Servers (Rsync-basiert)
- Normal Deployment - Regelmäßige Deployments via CI/CD (Git-basiert)
📖 Verwandte Dokumentation:
- Initial Deployment Guide - Schritt-für-Schritt Initial Deployment
- Code Deployment Workflow - Unterschiede zwischen Rsync und Git
- Code Changes Workflow - Wie Codeänderungen gepusht werden
- CI/CD Status - Aktueller Status der Pipeline
- Troubleshooting Guide - Häufige Probleme und Lösungen
Deployment-Flow
Initial Deployment (Rsync-basiert)
Local Repository
↓
1. Code Sync via rsync (sync-application-code.yml)
↓
2. Composer Dependencies installieren (install-composer-dependencies.yml)
↓
3. Docker Image Build & Push (build-initial-image.yml)
↓
4. Application Stack deployen (setup-infrastructure.yml)
↓
5. Container starten und verifizieren
Normal Deployment (Git-basiert / CI/CD)
CI/CD Pipeline (Gitea Actions)
↓
1. Tests & Build
↓
2. Docker Image Build & Push zur Registry
↓
3. Code deployen via Git (deploy-application-code.yml)
↓
4. Ansible Playbook ausführen (deploy-update.yml)
↓
5. Application Stack aktualisieren
Secret Handling für Redis
- Die Redis-Zugangsdaten liegen verschlüsselt in
deployment/ansible/secrets/production.vault.ymlunter dem Schlüsselvault_redis_password. - Die Application-Rolle (
roles/application/tasks/sync.yml) bricht den Deploy ab, wenn kein Passwort aus dem Vault oder via-e redis_password=...vorhanden ist. - Während des Deployments wird das Passwort in
stacks/production/.envgeschrieben und steht damit allen PHP-Containern überREDIS_PASSWORDzur Verfügung. - Docker Secrets mit
REDIS_PASSWORD_FILEwerden weiterhin unterstützt, da der Entry-Point das Secret lädt bevor PHP-FPM startet. - Das
APP_KEYstammt ebenfalls aus dem Vault (vault_app_key); der Deploy stoppt, falls kein Schlüssel hinterlegt ist. - Weitere sicherheitskritische Variablen (z. B.
VAULT_ENCRYPTION_KEY) werden aus dem Vault übernommen und in die generierte.envgeschrieben, damit die Container-Konfiguration 1:1 mit dem Projekt-Template übereinstimmt.
Detaillierter Ablauf
Phase 1: CI/CD Pipeline (.gitea/workflows/production-deploy.yml)
Job 1: Tests
- PHP 8.3 Setup
- Composer Dependencies installieren
- Pest Tests ausführen
- PHPStan Code Quality Check
- Code Style Check
Job 2: Build & Push
- Docker Image Build (Dockerfile.production)
- Image mit Tags pushen:
- registry.michaelschiemer.de/framework:latest
- registry.michaelschiemer.de/framework:<tag>
- registry.michaelschiemer.de/framework:git-<short-sha>
Job 3: Deploy (Ansible)
Schritt 1: Code Checkout
git clone <repository> /workspace/repo
Schritt 2: SSH Setup
# SSH Private Key aus Secret wird in ~/.ssh/production geschrieben
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
Schritt 3: Ansible Installation
apt-get install -y ansible
Schritt 4: Ansible Playbook ausführen
cd /workspace/repo/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/deploy-update.yml \
-e "image_tag=<generated-tag>" \
-e "git_commit_sha=<commit-sha>" \
-e "deployment_timestamp=<timestamp>" \
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}"
Phase 2: Ansible Deployment (deploy-update.yml)
Das Ansible Playbook führt folgende Schritte auf dem Production-Server aus:
Pre-Tasks
1. Secrets laden (optional)
- Lädt Registry Credentials aus Ansible Vault (falls vorhanden)
- Oder verwendet Credentials aus Workflow-Variablen
2. Docker Service prüfen
- Verifiziert, dass Docker läuft
- Startet Docker falls notwendig
3. Verzeichnisse erstellen
- Application Stack Path: ~/deployment/stacks/production
- Backup Verzeichnis: ~/deployment/backups/<timestamp>/
Tasks
1. Backup aktueller Deployment-Status
# Speichert aktuellen Container-Status
docker compose -f docker-compose.base.yml -f docker-compose.production.yml \
ps --format json > backups/<timestamp>/current_containers.json
# Speichert aktuelle docker-compose.yml Konfiguration
docker compose -f docker-compose.base.yml -f docker-compose.production.yml \
config > backups/<timestamp>/docker-compose-config.yml
2. Docker Registry Login
# Login zur privaten Registry mit Credentials
docker login registry.michaelschiemer.de \
-u <registry-username> \
-p <registry-password>
3. Neues Image Pullen
# Pullt das neue Image von der Registry
docker pull registry.michaelschiemer.de/framework:<tag>
# Beispiel:
# registry.michaelschiemer.de/framework:abc1234-1696234567
4. docker-compose.yml aktualisieren
Wichtig: Das Playbook aktualisiert die docker-compose.yml Datei direkt auf dem Server!
# Vorher:
services:
app:
image: registry.michaelschiemer.de/framework:latest
# Nachher (wenn image_tag != 'latest'):
services:
app:
image: registry.michaelschiemer.de/framework:<tag>
Regex-Replace:
regexp: '^(\s+image:\s+){{ app_image }}:.*$'
replace: '\1{{ app_image }}:{{ image_tag }}'
Betroffene Services (werden alle aktualisiert):
app(PHP-FPM) - Zeile 6:image: registry.michaelschiemer.de/framework:latestqueue-worker(Queue Worker) - Zeile 120:image: registry.michaelschiemer.de/framework:latestscheduler(Scheduler) - Zeile 165:image: registry.michaelschiemer.de/framework:latest
Hinweis:
- Alle drei Services verwenden das gleiche Image, daher werden alle mit dem neuen Tag aktualisiert
- Der Regex matched alle Zeilen die mit
image: registry.michaelschiemer.de/framework:beginnen nginxundredisbleiben unverändert (verwenden andere Images)
5. Application Stack neu starten
docker compose -f docker-compose.base.yml -f docker-compose.production.yml \
up -d \
--pull always \
--force-recreate \
--remove-orphans
Was passiert dabei:
--pull always: Zieht Images immer neu (auch wenn schon vorhanden)--force-recreate: Erstellt Container immer neu, auch wenn Konfiguration unverändert ist--remove-orphans: Entfernt Container, die nicht mehr in der Compose-Datei definiert sind-d: Läuft im Hintergrund (detached)
Container-Neustart-Reihenfolge:
- Abhängigkeiten werden gestoppt (app hängt von redis ab)
- Container werden neu erstellt mit neuem Image
- Container werden in korrekter Reihenfolge gestartet (redis → app → nginx)
- Health-Checks werden ausgeführt
6. Warten auf Health-Checks
# Wartet 60 Sekunden auf Container-Gesundheit
wait_for:
timeout: 60
7. Health-Status prüfen
# Prüft alle Container-Health-Status
docker compose ps --format json | \
jq -r '.[] | select(.Health != "healthy" and .Health != "") | "\(.Name): \(.Health)"'
8. Deployment-Metadaten speichern
~/deployment/backups/<timestamp>/deployment_metadata.txt
Inhalt:
Deployment Timestamp: 2025-10-31T02:35:04Z
Git Commit: abc1234...
Image Tag: abc1234-1696234567
Deployed Image: registry.michaelschiemer.de/framework:abc1234-1696234567
Image Pull: SUCCESS
Stack Deploy: UPDATED
Health Status: All services healthy
9. Alte Backups aufräumen
# Behält nur die letzten X Backups (Standard: 5)
ls -dt */ | tail -n +6 | xargs -r rm -rf
Phase 3: Application Stack Services
Der Application Stack besteht aus mehreren Services:
Services im Stack
1. app (PHP-FPM Application)
- Image:
registry.michaelschiemer.de/framework:<tag> - Container:
app - Health Check:
php-fpm-healthcheck - Netzwerk:
app-internal - Abhängigkeiten:
redis(condition: service_healthy)
2. nginx (Web Server)
- Image:
nginx:1.25-alpine - Container:
nginx - Health Check:
wget --spider http://localhost/health - Netzwerke:
traefik-public,app-internal - Abhängigkeiten:
app(condition: service_healthy) - Traefik Labels für Routing
3. redis (Cache/Session/Queue)
- Image:
redis:7-alpine - Container:
redis - Health Check:
redis-cli --raw incr ping - Netzwerk:
app-internal
4. queue-worker (Background Jobs)
- Image:
registry.michaelschiemer.de/framework:<tag>(gleiches wie app) - Container:
queue-worker - Health Check:
pgrep -f 'queue:work' - Netzwerk:
app-internal - Command:
php console.php queue:work - Abhängigkeiten:
app,redis
5. scheduler (Cron Jobs)
- Image:
registry.michaelschiemer.de/framework:<tag>(gleiches wie app) - Container:
scheduler - Health Check:
pgrep -f 'scheduler:run' - Netzwerk:
app-internal - Command:
php console.php scheduler:run - Abhängigkeiten:
app,redis
Container-Neustart-Details
Was passiert bei docker compose up -d --force-recreate?
1. Container-Stop:
- Laufende Container werden gestoppt
- Volumes bleiben erhalten (persistente Daten)
- Netzwerke bleiben erhalten
2. Container-Entfernung:
- Alte Container werden entfernt
- Neue Container werden mit neuem Image erstellt
3. Container-Start:
- Container starten in Abhängigkeits-Reihenfolge
- Health-Checks werden ausgeführt
- Falls Health-Check fehlschlägt, wird Container neu gestartet (restart: unless-stopped)
4. Zero-Downtime?
- Teilweise: Container werden nacheinander neu gestartet
nginxwartet aufappHealth-Check- Während Neustart kann es zu kurzen Verbindungsfehlern kommen
- Kein echter Blue-Green-Deployment (das wäre möglich, aber nicht implementiert)
Konfigurationsvariablen
Inventory-Variablen (inventory/production.yml)
app_image: "registry.michaelschiemer.de/framework"
docker_registry_url: "registry.michaelschiemer.de"
backups_path: "~/deployment/backups"
max_rollback_versions: 5
deploy_user_home: "~/deployment"
Workflow-Variablen (aus CI/CD)
image_tag: "abc1234-1696234567" # Generiert aus: <short-sha>-<timestamp>
git_commit_sha: "abc1234567890..."
deployment_timestamp: "2025-10-31T02:35:04Z"
docker_registry_username: "<from-secret>"
docker_registry_password: "<from-secret>"
Beispiel-Deployment
Schritt-für-Schritt Beispiel
1. CI/CD Pipeline startet:
# Commit: abc1234...
# Image Tag generiert: abc1234-1696234567
2. Image wird gebaut und gepusht:
docker buildx build \
--tag registry.michaelschiemer.de/framework:latest \
--tag registry.michaelschiemer.de/framework:abc1234-1696234567 \
--tag registry.michaelschiemer.de/framework:git-abc1234 \
--push \
.
3. Ansible Playbook wird ausgeführt:
ansible-playbook -i inventory/production.yml \
playbooks/deploy-update.yml \
-e "image_tag=abc1234-1696234567" \
-e "git_commit_sha=abc1234567890..." \
-e "deployment_timestamp=2025-10-31T02:35:04Z"
4. Auf Production-Server:
# 1. Backup erstellen
mkdir -p ~/deployment/backups/2025-10-31T02-35-04Z
# 2. Registry Login
docker login registry.michaelschiemer.de -u admin -p <password>
# 3. Image Pullen
docker pull registry.michaelschiemer.de/framework:abc1234-1696234567
# 4. docker-compose.yml aktualisieren
# Vorher: image: registry.michaelschiemer.de/framework:latest
# Nachher: image: registry.michaelschiemer.de/framework:abc1234-1696234567
# 5. Stack neu starten
cd ~/deployment/stacks/production
docker compose up -d --pull always --force-recreate --remove-orphans
# 6. Health-Checks warten
sleep 60
# 7. Status prüfen
docker compose ps
# app: healthy
# nginx: healthy
# redis: healthy
# queue-worker: healthy
# scheduler: healthy
5. Deployment-Metadaten speichern:
cat > ~/deployment/backups/2025-10-31T02-35-04Z/deployment_metadata.txt <<EOF
Deployment Timestamp: 2025-10-31T02:35:04Z
Git Commit: abc1234567890...
Image Tag: abc1234-1696234567
Deployed Image: registry.michaelschiemer.de/framework:abc1234-1696234567
Image Pull: SUCCESS
Stack Deploy: UPDATED
Health Status: All services healthy
EOF
Wichtige Hinweise
1. Image-Tag-Update
Wichtig: Das Playbook aktualisiert die docker-compose.yml Datei direkt auf dem Server!
- Die Datei wird mit
replaceModul geändert - Alle Services mit dem Image
registry.michaelschiemer.de/framework:*werden aktualisiert - Das bedeutet:
app,queue-worker, undschedulerbekommen alle das neue Image
2. Force-Recreate
Achtung: --force-recreate startet Container immer neu, auch wenn Konfiguration unverändert ist.
- Container-IDs ändern sich
- Kurze Downtime möglich
- Neue Container bekommen neue IPs im Docker-Netzwerk
3. Health-Checks
Health-Checks sind kritisch:
- Container müssen "healthy" werden, sonst starten abhängige Container nicht
nginxwartet aufappHealth-Checkappwartet aufredisHealth-Check
Health-Check Timeouts:
app: 40s start_period, dann 30s intervalredis: 10s start_period, dann 30s intervalnginx: 10s start_period, dann 30s interval
4. Backups
Backup-Strategie:
- Jedes Deployment erstellt ein Backup-Verzeichnis
- Enthält: Container-Status, docker-compose.yml, Deployment-Metadaten
- Alte Backups werden automatisch gelöscht (Standard: Behält 5 Backups)
Backup-Pfad:
~/deployment/backups/
├── 2025-10-31T02-35-04Z/
│ ├── current_containers.json
│ ├── docker-compose-config.yml
│ └── deployment_metadata.txt
├── 2025-10-31T01-20-15Z/
│ └── ...
└── ...
Rollback-Prozess
Falls das Deployment fehlschlägt:
Automatischer Rollback (via Workflow)
# Im Workflow: Rollback on failure
if: failure() && steps.health.outcome == 'failure'
run: |
ansible-playbook -i inventory/production.yml \
playbooks/rollback.yml
Manueller Rollback
cd ~/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/rollback.yml \
-e "rollback_timestamp=2025-10-31T01-20-15Z"
Was passiert beim Rollback:
- Liest vorheriges Backup
- Pullt altes Image
- Aktualisiert
docker-compose.ymlmit altem Image-Tag - Startet Stack neu mit altem Image
Verbesserungsmöglichkeiten
1. Blue-Green Deployment
- Zwei parallele Stacks (blue/green)
- Traffic-Switching via Traefik
- Zero-Downtime möglich
2. Rolling Updates
- Container werden nacheinander aktualisiert
- Reduzierte Downtime
3. Database-Migration vor Deployment
- Migrationen werden aktuell nach Deployment ausgeführt
- Besser: Migrationen vor Deployment testen
4. Canary Deployment
- Neue Version zuerst auf Subset der Traffic
- Graduelle Rollout
Troubleshooting
Container starten nicht
# Logs prüfen
docker compose -f ~/deployment/stacks/production/docker-compose.yml logs
# Health-Check-Status prüfen
docker compose ps
Image wird nicht gepullt
# Registry-Login testen
docker login registry.michaelschiemer.de -u admin -p <password>
# Image manuell pullen
docker pull registry.michaelschiemer.de/framework:<tag>
docker-compose.yml wurde nicht aktualisiert
# Prüfe ob Regex korrekt ist
grep -E "image:\s+registry.michaelschiemer.de/framework" \
docker-compose.base.yml docker-compose.production.yml
# Prüfe Backup für vorherige Version
ls -la ~/deployment/backups/
Zusammenfassung
Deployment-Ablauf:
- CI/CD Pipeline baut Image und pusht es zur Registry
- Ansible Playbook wird auf Production-Server ausgeführt
- Neues Image wird gepullt
docker-compose.ymlwird mit neuem Image-Tag aktualisiert- Application Stack wird mit
--force-recreateneu gestartet - Health-Checks werden ausgeführt
- Deployment-Metadaten werden gespeichert
- Workflow führt abschließenden Health-Check aus
Alle Services erhalten das neue Image:
app(PHP-FPM)queue-worker(Queue Worker)scheduler(Scheduler)
Zero-Downtime: Nein, Container werden neu gestartet (kurze Downtime möglich)
Rollback: Automatisch via Workflow oder manuell via Rollback-Playbook