ci: setup CI/CD pipeline with Gitea Actions and secrets configuration

This commit is contained in:
2025-10-31 01:31:44 +01:00
parent 38baaca06b
commit 55c04e4fd0
28 changed files with 2113 additions and 958 deletions

View File

@@ -0,0 +1,86 @@
# Automatisches Secrets-Setup
## Schnellstart
```bash
# Interaktives Script (fragt nach Token)
bash scripts/setup-gitea-secrets-interactive.sh
```
Das Script:
1. Fragt nach einem Gitea Access Token
2. Setzt automatisch alle drei Secrets via API
## Token generieren
Falls noch kein Token vorhanden:
1. **Gehe zu Gitea Settings:**
```
https://git.michaelschiemer.de/user/settings/applications
```
2. **Klicke "Generate New Token"**
3. **Konfiguration:**
- Name: `secrets-setup` (oder beliebig)
- Disponível scopes:
- ✅ `write:repository` (mindestens)
- ✅ Oder wähle alle für volle Berechtigung
4. **Kopiere den Token** (wird nur einmal angezeigt!)
## Script ausführen
```bash
# Script starten
bash scripts/setup-gitea-secrets-interactive.sh
# Token eingeben (wird nicht angezeigt)
Gitea Token: <token-einfügen>
# Script setzt automatisch:
# ✅ REGISTRY_USER = admin
# ✅ REGISTRY_PASSWORD = registry-secure-password-3125
# ✅ SSH_PRIVATE_KEY = <aus ~/.ssh/production>
```
## Verifizierung
Nach erfolgreichem Setup:
1. **Prüfe in Gitea UI:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
2. **Sollte zeigen:**
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle drei Secrets sollten "Hidden" als Wert anzeigen.
## Troubleshooting
### "Token ungültig"
- Prüfe, ob Token korrekt kopiert wurde (keine Leerzeichen)
- Prüfe, ob Token die Berechtigung `write:repository` hat
### "Repository nicht gefunden"
- Prüfe Repository-Name: `michael/michaelschiemer`
- Prüfe, ob du Zugriff auf das Repository hast
### "HTTP 403 Forbidden"
- Token hat keine ausreichenden Berechtigungen
- Generiere neuen Token mit `write:repository` scope
### API nicht erreichbar
- Prüfe Gitea URL: `https://git.michaelschiemer.de`
- Prüfe Netzwerkverbindung
## Alternative: Manuelles Setup
Falls das automatische Setup nicht funktioniert:
Siehe `.gitea/workflows/QUICK_SECRETS_SETUP.md` für manuelle Anleitung.

View File

@@ -0,0 +1,90 @@
# CI/CD Pipeline - Setup Checklist
## ✅ Status
### 1. Repository Secrets in Gitea ✅ BEREIT
**Pfad**: Repository → Settings → Secrets
**Schnelles Setup:**
```bash
bash scripts/prepare-secrets.sh
```
**Erforderliche Secrets:**
- [ ] **REGISTRY_USER**: `admin`
- [ ] **REGISTRY_PASSWORD**: `registry-secure-password-2025`
- [ ] **SSH_PRIVATE_KEY**: Vollständiger Inhalt von `~/.ssh/production`
**Details**: Siehe `.gitea/workflows/QUICK_SECRETS_SETUP.md`
**Hinweis**: Alle Secrets müssen in Gitea konfiguriert werden, bevor die Pipeline läuft.
### 2. Docker Registry ✅
- ✅ Registry läuft auf `git.michaelschiemer.de:5000`
- ✅ Authentifizierung konfiguriert (admin/registry-secure-password-2025)
- ✅ Erreichbar via HTTP auf `127.0.0.1:5000`
- ✅ Image `framework` bereits vorhanden
**Registry-Test**:
```bash
curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog
# Erwartete Ausgabe: {"repositories":["framework"]}
```
### 3. Ansible Playbooks ✅
-`deploy-update.yml` - Aktualisiert für Docker Compose
-`rollback.yml` - Aktualisiert für Docker Compose
-`setup-infrastructure.yml` - Enthält Registry-Setup
-`sync-stacks.yml` - Synchronisiert Stack-Konfigurationen
### 4. Workflow-Konfiguration ✅
-`.gitea/workflows/production-deploy.yml` - Haupt-Workflow
-`Dockerfile.production` - Production Dockerfile erstellt
- ✅ Application Stack konfiguriert (`deployment/stacks/application/docker-compose.yml`)
## 📋 Nächste Schritte
### Schritt 1: Secrets in Gitea hinzufügen
Siehe `.gitea/workflows/SECRETS_SETUP.md` für detaillierte Anleitung.
### Schritt 2: Workflow manuell testen
1. Gehe zu: `https://git.michaelschiemer.de/<username>/michaelschiemer/actions`
2. Wähle "Production Deployment Pipeline"
3. Klicke "Run workflow"
4. Wähle Branch `main`
5. Beobachte die Ausführung
### Schritt 3: Automatisches Deployment testen
1. Push zu `main` Branch
2. Workflow sollte automatisch starten
3. Prüfe Logs in Gitea Actions
## 🔧 Troubleshooting
### Registry nicht erreichbar
```bash
# Prüfe Registry-Status
ssh deploy@94.16.110.151 "docker ps | grep registry"
# Teste Registry-Erreichbarkeit
ssh deploy@94.16.110.151 "curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog"
```
### Workflow schlägt bei Registry-Login fehl
- Prüfe, ob `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets korrekt gesetzt sind
- Prüfe, ob das Passwort in Gitea mit dem Server übereinstimmt
### Image Pull schlägt fehl
- Prüfe, ob das Image in der Registry existiert:
```bash
curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/framework/tags/list
```
- Prüfe Registry-Logs für Fehler
### Deployment schlägt fehl
- Prüfe Ansible-Logs im Workflow
- Prüfe, ob `deployment/stacks/application/docker-compose.yml` auf dem Server existiert
- Prüfe Docker-Logs auf dem Server:
```bash
ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose logs"
```

View File

@@ -0,0 +1,101 @@
# Manuelles Secrets-Setup - Einfache Anleitung
Da das automatische Setup Probleme macht, hier die manuelle Lösung:
## Schritt 1: Gehe zu Gitea Secrets-Seite
Öffne im Browser:
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
**Hinweis**: Falls diese Seite nicht existiert oder einen 404 gibt:
- Prüfe, ob das Repository wirklich `michael/michaelschiemer` heißt
- Prüfe, ob du Zugriff auf das Repository hast
- Prüfe, ob Actions aktiviert ist
## Schritt 2: Füge die drei Secrets hinzu
Für jedes Secret: **Klicke "New Secret"**, fülle aus, **Save**
### Secret 1: REGISTRY_USER
- **Name**: `REGISTRY_USER`
- **Value**: `admin`
- **Save**
### Secret 2: REGISTRY_PASSWORD
- **Name**: `REGISTRY_PASSWORD`
- **Value**: `registry-secure-password-2025`
- **Save**
### Secret 3: SSH_PRIVATE_KEY
- **Name**: `SSH_PRIVATE_KEY`
- **Value**: Führe aus `cat ~/.ssh/production` und kopiere den KOMPLETTEN Inhalt
```bash
cat ~/.ssh/production
```
**Wichtig**: Kopiere ALLES, inklusive:
- `-----BEGIN OPENSSH PRIVATE KEY-----`
- Alle Zeilen dazwischen
- `-----END OPENSSH PRIVATE KEY-----`
- **Save**
## Schritt 3: Verifizierung
Nach dem Setup sollten alle drei Secrets in der Liste erscheinen:
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle zeigen "Hidden" als Wert.
## Falls die Secrets-Seite nicht erreichbar ist
### Option A: Repository-Name prüfen
```bash
# Prüfe aktuelle Remote-URL
git remote get-url origin
# Sollte zeigen:
# https://git.michaelschiemer.de/<owner>/<repo>.git
```
Falls der Name anders ist, verwende die korrekte URL.
### Option B: Repository erstellen
Falls das Repository noch nicht existiert:
1. Gehe zu: `https://git.michaelschiemer.de/repos/new`
2. Erstelle das Repository `michaelschiemer`
3. Dann gehe zu: `https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions`
### Option C: Actions aktivieren
Falls Actions nicht aktiviert ist:
1. Gehe zu Repository Settings
2. Prüfe, ob "Actions" aktiviert ist
3. Falls nicht, aktiviere es in den Repository-Einstellungen
## Nächster Schritt
Nach erfolgreichem Setup der Secrets:
1. **Teste den Workflow:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/actions
```
2. **Oder pushe einen Commit:**
```bash
git push origin main
```
Der Workflow sollte dann automatisch starten.

View File

@@ -0,0 +1,87 @@
# Secrets Setup mit Token - Schritt für Schritt
## ✅ Du hast bereits ein Token - Perfekt!
## Option 1: Automatisches Setup (Empfohlen)
Führe einfach dieses Kommando aus (ersetze `<DEIN_TOKEN>` mit deinem Token):
```bash
bash scripts/setup-gitea-secrets-with-token.sh <DEIN_TOKEN>
```
**Beispiel:**
```bash
bash scripts/setup-gitea-secrets-with-token.sh ghp_1234567890abcdefghijklmnopqrstuvwxyz
```
Das Script:
1. ✅ Testet die API-Verbindung
2. ✅ Setzt automatisch `REGISTRY_USER` = `admin`
3. ✅ Setzt automatisch `REGISTRY_PASSWORD` = `registry-secure-password-2025`
4. ✅ Setzt automatisch `SSH_PRIVATE_KEY` = Inhalt von `~/.ssh/production`
## Option 2: Manuell über Gitea UI
Falls das automatische Setup nicht funktioniert:
### Schritt 1: Gehe zu Secrets-Seite
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
### Schritt 2: Füge jedes Secret einzeln hinzu
**REGISTRY_USER:**
1. Klicke "New Secret"
2. Name: `REGISTRY_USER`
3. Value: `admin`
4. Save
**REGISTRY_PASSWORD:**
1. Klicke "New Secret"
2. Name: `REGISTRY_PASSWORD`
3. Value: `registry-secure-password-2025`
4. Save
**SSH_PRIVATE_KEY:**
1. Klicke "New Secret"
2. Name: `SSH_PRIVATE_KEY`
3. Value: Kompletter Inhalt von `~/.ssh/production`
```bash
cat ~/.ssh/production
```
(Kopiere ALLES, inklusive `-----BEGIN` und `-----END` Zeilen)
4. Save
## Verifizierung
Nach dem Setup:
1. **Prüfe in Gitea UI:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
2. **Sollte zeigen:**
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle drei Secrets sollten "Hidden" als Wert anzeigen.
## Troubleshooting
### "API-Verbindung fehlgeschlagen"
- Prüfe Token-Kopierung (keine Leerzeichen)
- Prüfe Token-Berechtigung: `write:repository` Scope nötig
- Prüfe Repository-Name: `michael/michaelschiemer`
### "HTTP 403 Forbidden"
- Token hat keine ausreichenden Berechtigungen
- Generiere neuen Token mit `write:repository` scope
### "HTTP 404 Not Found"
- Repository existiert nicht oder falscher Name
- Prüfe: `https://git.michaelschiemer.de/michael/michaelschiemer`

View File

@@ -0,0 +1,119 @@
# ✅ Secrets erfolgreich gesetzt!
## Status
✅ Repository erstellt
✅ Secrets konfiguriert:
- REGISTRY_USER
- REGISTRY_PASSWORD
- SSH_PRIVATE_KEY
## 🚀 Nächster Schritt: CI/CD Workflow testen
### Option 1: Workflow manuell triggern (Empfohlen)
1. **Gehe zu Actions:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/actions
```
2. **Workflow auswählen:**
- Suche nach "Production Deployment Pipeline"
- Klicke auf den Workflow
3. **Workflow starten:**
- Klicke "Run workflow" (rechts oben)
- Branch: `main`
- `skip_tests`: deaktiviert (Tests sollen laufen)
- Klicke "Run workflow"
4. **Logs beobachten:**
- Prüfe jeden Schritt in den Logs
- Er_warte ~8-15 Minuten für komplette Ausführung
### Option 2: Automatisches Deployment via Commit
```bash
# Stelle sicher, dass alles committed ist
git add .
git commit -m "chore: CI/CD pipeline setup complete"
# Push zu main - startet automatisch den Workflow
git push origin main
```
## 📊 Workflow-Schritte
Der Workflow führt folgende Schritte aus:
1. **Tests** (~2-5 Min)
- PHP Setup
- Composer Dependencies
- Tests ausführen
2. **Build** (~3-5 Min)
- Multi-Stage Docker Build
- Composer Dependencies (Production)
- Frontend Build (npm)
- Final Production Image
3. **Push** (~1-2 Min)
- Docker Login zur Registry
- Image Tag generieren
- Image zur Registry pushen
4. **Deploy** (~2-4 Min)
- Ansible Playbook ausführen
- Image auf Production pullen
- Application Stack aktualisieren
- Services neu starten
**Gesamtzeit: ~8-15 Minuten**
## ✅ Erfolgreiche Ausführung erkennen
Der Workflow ist erfolgreich, wenn:
- ✅ Alle Jobs grün sind
- ✅ Keine Fehler in den Logs
- ✅ "Deploy via Ansible" erfolgreich
- ✅ Application läuft auf Production-Server
## 🔍 Verifizierung nach Deployment
```bash
# Prüfe Application-Status
ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose ps"
# Prüfe Logs
ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose logs --tail=50"
# Prüfe Health Endpoint
curl -k https://michaelschiemer.de/health
```
## 🐛 Troubleshooting
### Workflow startet nicht
- Prüfe, ob `.gitea/workflows/production-deploy.yml` im Repository ist
- Prüfe Workflow-Syntax
### "Secret not found" Fehler
- Prüfe Secrets-Seite: `https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions`
- Prüfe, ob Namen exakt übereinstimmen (Groß-/Kleinschreibung!)
### Registry Login fehlgeschlagen
- Prüfe `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets
- Teste Registry: `curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog`
### Deployment fehlgeschlagen
- Prüfe `SSH_PRIVATE_KEY` Secret
- Prüfe Ansible-Verbindung: `ssh deploy@94.16.110.151 "echo OK"`
- Prüfe Server-Logs
## 🎉 Nach erfolgreichem Test
Du kannst jetzt:
- ✅ Commits pushen → Automatisches Deployment
- ✅ Workflow manuell triggern für kontrollierte Deployments
- ✅ Branch-Protection aktivieren für sichere Deployments

View File

@@ -0,0 +1,104 @@
# Quick Secrets Setup für Gitea CI/CD
## Zusammenfassung der benötigten Werte
### REGISTRY_USER
```
admin
```
### REGISTRY_PASSWORD
```
registry-secure-password-2025
```
### SSH_PRIVATE_KEY
Kopiere den kompletten Inhalt von φ~/.ssh/production`:
```bash
# Zeige SSH Key Inhalt (für Copy-Paste)
cat ~/.ssh/production
```
**Wichtig**: Der komplette Inhalt muss kopiert werden, inklusive:
- `-----BEGIN OPENSSH PRIVATE KEY-----`
- Alle Zeilen dazwischen
- `-----END OPENSSH PRIVATE KEY-----`
## Schnelle Vorbereitung
Führe das Helper-Script aus, um alle Werte anzuzeigen:
```bash
bash scripts/prepare-secrets.sh
```
Dies zeigt alle drei Secrets an, die du kopieren kannst.
## Manuelle Einrichtung in Gitea
1. **Gehe zu Repository Settings:**
```
https://git.michaelschiemer.de/<username>/michaelschiemer/settings/secrets
```
2. **Klicke auf "New Secret"**
3. **Füge jedes Secret hinzu:**
**REGISTRY_USER:**
- Name: `REGISTRY_USER`
- Value: `admin`
- Save
**REGISTRY_PASSWORD:**
- Name: `REGISTRY_PASSWORD`
- Value: `registry-secure-password-2025`
- Save
**SSH_PRIVATE_KEY:**
- Name: `SSH_PRIVATE_KEY`
- Value: `<kompletter Inhalt von cat ~/.ssh/production>`
- Save
## Verifizierung
Nach dem Setup sollten alle drei Secrets in der Liste erscheinen:
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle zeigen "Hidden" als Wert.
## Nächster Schritt
Sobald die Secrets konfiguriert sind:
1. **Workflow manuell testen:**
```
https://git.michaelschiemer.de/<username>/michaelschiemer/actions
```
- Wähle "Production Deployment Pipeline"
- Klicke "Run workflow"
- Wähle Branch `main`
2. **Oder automatisches Deployment testen:**
- Pushe einen Commit zu `main`
- Workflow startet automatisch
## Troubleshooting
### Secrets werden nicht erkannt
- Prüfe, ob die Namen exakt übereinstimmen (Groß-/Kleinschreibung!)
- Prüfe, ob keine Leerzeichen am Anfang/Ende
### SSH Key Fehler
- Stelle sicher, dass der komplette Key kopiert wurde
- Prüfe, dass `-----BEGIN` und `-----END` Zeilen enthalten sind
### Registry Login Fehler
- Prüfe, ob `REGISTRY_USER` und `REGISTRY_PASSWORD` korrekt sind
- Teste Registry-Erreichbarkeit:
```bash
curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog
```

View File

@@ -0,0 +1,30 @@
# Docker Registry Information
## Registry Details
- **URL**: `git.michaelschiemer.de:5000` (intern) oder `registry.michaelschiemer.de` (via Traefik)
- **Standard Credentials**:
- **Username**: `admin`
- **Password**: `registry-secure-password-2025`
⚠️ **WICHTIG**: Das Passwort sollte in Produktion geändert werden!
## Für Gitea Secrets
Verwende folgende Werte in den Gitea Repository Secrets:
- **REGISTRY_USER**: `admin`
- **REGISTRY_PASSWORD**: `registry-secure-password-2025` (oder das aktuell gesetzte Passwort)
## Registry Test
```bash
# Login testen
echo "registry-secure-password-2025" | docker login git.michaelschiemer.de:5000 -u admin --password-stdin
# Images auflisten
curl -u admin:registry-secure-password-2025 http://git.michaelschiemer.de:5000/v2/_catalog
# Oder via Traefik (HTTPS)
curl -u admin:registry-secure-password-2025 https://registry.michaelschiemer.de/v2/_catalog
```

View File

@@ -0,0 +1,86 @@
# Gitea Repository Secrets Setup
## Erforderliche Secrets
Diese Secrets müssen in Gitea konfiguriert werden unter:
**Repository → Settings → Secrets**
### 1. REGISTRY_USER
- **Beschreibung**: Benutzername für Docker Registry Login
- **Typ**: String
- **Wert**: Standardmäßig `admin` oder der Benutzername für die Registry
- **Verwendung**: Docker Registry Authentication beim Image Push
### 2. REGISTRY_PASSWORD
- **Beschreibung**: Passwort für Docker Registry Login
- **Typ**: Password (versteckt)
- **Wert**: Das Passwort für die Docker Registry auf `git.michaelschiemer.de:5000`
- **Verwendung**: Docker Registry Authentication beim Image Push
### 3. SSH_PRIVATE_KEY
- **Beschreibung**: SSH Private Key für Zugriff auf Production Server
- **Typ**: SSH Key (versteckt)
- **Wert**: Der komplette Inhalt der SSH-Private-Key-Datei (~/.ssh/production)
- **Verwendung**: SSH-Verbindung zum Production-Server für Ansible Deployment
## Setup-Anleitung
### Schritt 1: SSH Key erstellen/exportieren
```bash
# Falls noch nicht vorhanden, SSH Key für Production erstellen
ssh-keygen -t ed25519 -f ~/.ssh/production -C "gitea-ci-cd"
# SSH Key Inhalt anzeigen (für Copy-Paste)
cat ~/.ssh/production
```
**⚠️ Wichtig**: Der komplette Inhalt der Datei (inkl. `-----BEGIN OPENSSH PRIVATE KEY-----` und `-----END OPENSSH PRIVATE KEY-----`) muss in das Secret eingefügt werden.
### Schritt 2: Docker Registry Credentials prüfen
Die Registry läuft auf dem Production-Server. Prüfe die Credentials:
```bash
# SSH zum Production-Server
ssh deploy@94.16.110.151
# Prüfe, ob Registry läuft
docker ps | grep registry
# Prüfe Registry-Konfiguration (falls vorhanden)
cat ~/deployment/stacks/registry/docker-compose.yml 2>/dev/null || echo "Registry Config nicht gefunden"
```
**Hinweis**: Falls die Registry noch nicht konfiguriert ist, müssen die Credentials festgelegt werden.
### Schritt 3: Secrets in Gitea hinzufügen
1. Gehe zu: `https://git.michaelschiemer.de/<username>/michaelschiemer/settings/secrets`
2. Klicke auf **"Add Secret"**
3. Füge jedes Secret einzeln hinzu:
**REGISTRY_USER**:
- Name: `REGISTRY_USER`
- Value: `admin` (oder der tatsächliche Registry-Benutzername)
- Save
**REGISTRY_PASSWORD**:
- Name: `REGISTRY_PASSWORD`
- Value: `<registry-password>`
- Save
**SSH_PRIVATE_KEY**:
- Name: `SSH_PRIVATE_KEY`
- Value: `<kompletter-inhalt-von-~/.ssh/production>`
- Save
### Schritt 4: Secrets verifizieren
Nach dem Hinzufügen sollten alle drei Secrets in der Liste erscheinen mit "Hidden" als Wert.
**✅ Checkliste**:
- [ ] REGISTRY_USER hinzugefügt
- [ ] REGISTRY_PASSWORD hinzugefügt
- [ ] SSH_PRIVATE_KEY hinzugefügt
- [ ] Alle Secrets zeigen "Hidden" als Wert

View File

@@ -0,0 +1,132 @@
# CI/CD Workflow Testen
## ✅ Secrets sind konfiguriert - Perfekt!
## Option 1: Workflow manuell triggern (Empfohlen)
### Schritt 1: Gehe zu Actions
Öffne im Browser:
```
https://git.michaelschiemer.de/michael/michaelschiemer renewal/actions
```
### Schritt 2: Workflow auswählen
1. Suche nach "Production Deployment Pipeline"
2. Klicke auf den Workflow
### Schritt 3: Workflow manuell starten
1. Klicke auf "Run workflow" (rechts oben)
2. Wähle Branch: `main`
3. Optional: `skip_tests` deaktiviert lassen (Tests sollen laufen)
4. Klicke "Run workflow"
### Schritt 4: Logs beobachten
Der Workflow führt folgende Schritte aus:
1. **Checkout code** - Code wird ausgecheckt
2. **Run Tests** - PHP Tests werden ausgeführt
3. **Build Docker Image** - Docker Image wird gebaut
4. **Push to Registry** - Image wird zur Registry gepusht
5. **Deploy via Ansible** - Deployment auf Production-Server
**Beobachte die Logs** und prüfe jeden Schritt!
## Option 2: Automatisches Deployment via Commit
### Test-Commit pushen
```bash
# Stelle sicher, dass alles committed ist
git add .
git commit -m "test: CI/CD workflow test" || echo "Keine Änderungen"
# Push zu main Branch
git push origin main
```
Der Workflow startet automatisch nach dem Push.
## Was passiert beim Workflow?
### 1. Tests (ca. 2-5 Minuten)
- PHP Version Setup
- Composer Dependencies installieren
- Tests ausführen
### 2. Build (ca. 3-5 Minuten)
Live - Multi-Stage Docker Build:
- Composer Dependencies (Production)
- Frontend Build (npm)
- Finales Production Image
### 3. Push (ca. 1-2 Minuten)
- Docker Login zur Registry
- Image Tag generieren (SHA + Timestamp)
- Image zur Registry pushen
### 4. Deploy (ca. 2-4 Minuten)
- Ansible Playbook ausführen
- Image auf Production-Server pullen
- Application Stack aktualisieren
- Services neu starten
**Gesamtzeit: ~8-15 Minuten**
## Workflow-Status prüfen
### In Gitea UI:
```
https://git.michaelschiemer.de/michael/michaelschiemer/actions
```
### Via Command Line:
```bash
# Prüfe ob Workflow läuft
curl -s -H "Authorization: token <DEIN_TOKEN>" \
"https://git.michaelschiemer.de/api/v1/repos/michael/michaelschiemer/actions/runs" | \
jq '.workflow_runs[0] | {status, conclusion, created_at}'
```
## Erfolgreiche Ausführung erkennen
Der Workflow ist erfolgreich, wenn:
✅ Alle Jobs grün sind
✅ Keine Fehler in den Logs
✅ Letzter Schritt "Deploy via Ansible" erfolgreich
✅ Application läuft auf Production-Server
## Troubleshooting
### Workflow startet nicht
- Prüfe, ob `.gitea/workflows/production-deploy.yml` im Repository ist
- Prüfe, ob Workflow-Syntax korrekt ist
- Prüfe Gitea Actions ist aktiviert
### "Secret not found" Fehler
- Prüfe, ob alle drei Secrets gesetzt sind
- Prüfe, ob Namen exakt übereinstimmen (Groß-/Kleinschreibung!)
### Registry Login fehlgeschlagen
- Prüfe `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets
- Prüfe Registry erreichbar: `curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog`
### Deployment fehlgeschlagen
- Prüfe `SSH_PRIVATE_KEY` Secret
- Prüfe Ansible-Verbindung zum Server
- Prüfe Server-Logs: `ssh deploy@94.16.110.151 "docker compose -f ~/deployment/stacks/application/docker-compose.yml logs"`
## Nächste Schritte nach erfolgreichem Test
1. ✅ Workflow funktioniert
2. ✅ Automatisches Deployment getestet
3. ✅ Production-Stack läuft
**Du kannst jetzt:**
- Normale Commits pushen → Automatisches Deployment
- Workflow manuell triggern für kontrollierte Deployments
- Branch-Protection aktivieren für sichere Deployments

View File

@@ -0,0 +1,75 @@
# Token-Probleme beheben
## Problem
Das Token hat nicht die richtigen Scopes oder das Repository wurde nicht gefunden.
## Lösung: Neuen Token mit richtigen Scopes erstellen
### Schritt 1: Token neu generieren
1. **Gehe zu Gitea Settings:**
```
https://git.michaelschiemer.de/user/settings/applications
```
2. **Falls bereits ein Token existiert:**
- Lösche den alten Token (falls nötig)
- Oder erstelle einen neuen mit anderen Namen
3. **Klicke "Generate New Token"**
4. **WICHTIG - Diese Scopes aktivieren:**
- ✅ `read:user` (mindestens erforderlich)
- ✅ `write:repository` (für Secrets schreiben)
- ✅ Oder wähle **alle Scopes** für volle Berechtigung
5. **Token kopieren** (wird nur einmal angezeigt!)
### Schritt 2: Repository-Name prüfen
Prüfe, ob das Repository wirklich `michael/michaelschiemer` heißt:
```bash
# Prüfe Remote-URL
git remote get-url origin
# Sollte zeigen:
# https://git.michaelschiemer.de/michael/michaelschiemer.git
```
Falls der Name anders ist, setze die Umgebungsvariable:
```bash
REPO_OWNER=<owner> REPO_NAME=<name> bash scripts/setup-gitea-secrets-with-token.sh <token>
```
### Schritt 3: Script erneut ausführen
```bash
bash scripts/setup-gitea-secrets-with-token.sh <NEUER_TOKEN>
```
## Alternative: Manuelles Setup über UI
Falls das automatische Setup weiterhin Probleme macht:
1. **Gehe zu:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
2. **Füge manuell hinzu:**
- `REGISTRY_USER` = `admin`
- `REGISTRY_PASSWORD` = `registry-secure-password-2025`
- `SSH_PRIVATE_KEY` = `cat ~/.ssh/production`
## Troubleshooting
### "token does not have at least one of required scope(s)"
→ Token benötigt `read:user` Scope - neuen Token mit diesem Scope generieren
### "The target couldn't be found" (404)
→ Repository existiert nicht oder falscher Name - prüfe Repository-URL
### "404 page not found" bei Secrets-Endpoint
→ Actions möglicherweise nicht aktiviert - prüfe in Gitea Admin-Panel

View File

@@ -1,102 +0,0 @@
name: CI/CD Pipeline für michaelschiemer.de
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY_URL: docker-registry:5000 # Container network access
IMAGE_NAME: michaelschiemer
jobs:
test:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
services:
redis:
image: redis:8-alpine
mariadb:
image: mariadb:latest
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: gd, zip, pdo, pdo_mysql, opcache, pcntl, posix, shmop, redis
tools: composer
- name: Install Dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Build Frontend Assets
run: npm install && npm run build
- name: Run Tests
run: ./vendor/bin/pest
env:
DB_HOST: mariadb
DB_PORT: 3306
DB_DATABASE: test
DB_USERNAME: root
DB_PASSWORD: test
REDIS_HOST: redis
REDIS_PORT: 6379
build:
needs: test
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Login to Private Registry
run: < /dev/null |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY_URL }} -u admin --password-stdin
- name: Determine Image Tag
id: tag
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "tag=latest" >> $GITHUB_OUTPUT
else
echo "tag=develop" >> $GITHUB_OUTPUT
fi
- name: Build and Push Images
run: |
# Build and push PHP image
if [ -f docker/php/Dockerfile ]; then
docker build -t ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/php:${{ steps.tag.outputs.tag }} -f docker/php/Dockerfile .
docker push ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/php:${{ steps.tag.outputs.tag }}
fi
# Build and push Nginx image
if [ -f docker/nginx/Dockerfile ]; then
docker build -t ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/nginx:${{ steps.tag.outputs.tag }} -f docker/nginx/Dockerfile .
docker push ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/nginx:${{ steps.tag.outputs.tag }}
fi
# Build and push Worker image
if [ -f docker/worker/Dockerfile ]; then
docker build -t ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/worker:${{ steps.tag.outputs.tag }} -f docker/worker/Dockerfile .
docker push ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/worker:${{ steps.tag.outputs.tag }}
fi

View File

@@ -1,51 +0,0 @@
name: CI
on:
push:
pull_request:
jobs:
quality:
name: PHP Quality Checks
runs-on: ubuntu-latest
container:
image: composer:2
steps:
# Gitea runners usually have the repo checked out already. We avoid marketplace actions.
- name: Show versions
run: |
php -v
composer --version
- name: Install dependencies
run: |
composer install --no-interaction --prefer-dist --no-progress
- name: Code style (non-blocking for initial adoption)
run: |
if composer run -q cs 2>/dev/null; then
composer cs || true
else
echo "No 'cs' script found, skipping."
fi
- name: PHPStan
run: |
if composer run -q phpstan 2>/dev/null; then
composer phpstan
else
echo "No 'phpstan' script found, attempting vendor bin..."
./vendor/bin/phpstan analyse || exit 1
fi
- name: Pest tests
env:
XDEBUG_MODE: off
run: |
if [ -x ./vendor/bin/pest ]; then
./vendor/bin/pest -q
elif [ -x ./vendor/bin/phpunit ]; then
./vendor/bin/phpunit
else
echo "No test runner found."
exit 1
fi
- name: Composer audit (non-blocking initially)
run: |
composer audit --no-interaction || true

View File

@@ -0,0 +1,74 @@
name: Update Production Secrets
on:
workflow_dispatch:
inputs:
vault_password:
description: 'Ansible Vault Password'
required: true
type: password
env:
DEPLOYMENT_HOST: 94.16.110.151
jobs:
deploy-secrets:
name: Deploy Secrets to Production
runs-on: ubuntu-latest
environment:
name: production-secrets
url: https://michaelschiemer.de
steps:
- name: Checkout deployment configuration
uses: actions/checkout@v4
with:
sparse-checkout: |
deployment/ansible
sparse-checkout-cone-mode: false
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
- name: Install Ansible
run: |
sudo apt-get update
sudo apt-get install -y ansible
- name: Create vault password file
run: |
echo "${{ github.event.inputs.vault_password }}" > /tmp/.vault_pass
chmod 600 /tmp/.vault_pass
- name: Deploy secrets via Ansible
run: |
cd deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/setup-production-secrets.yml \
--vault-password-file /tmp/.vault_pass
- name: Cleanup vault password
if: always()
run: |
rm -f /tmp/.vault_pass
- name: Verify secrets deployment
run: |
ssh -i ~/.ssh/production deploy@${{ env.DEPLOYMENT_HOST }} \
"docker secret ls && test -f /home/deploy/secrets/.env.production"
- name: Notify deployment success
if: success()
run: |
echo "✅ Secrets deployed successfully to production"
echo "Services will be restarted automatically"
- name: Notify deployment failure
if: failure()
run: |
echo "❌ Secrets deployment failed"
echo "Check Ansible logs for details"

View File

@@ -1,14 +1,15 @@
---
- name: Deploy Application Update
- name: Deploy Application Update via Docker Compose
hosts: production
gather_facts: yes
become: yes
become: no
vars:
# These should be passed via -e from CI/CD
image_tag: "{{ image_tag | default('latest') }}"
git_commit_sha: "{{ git_commit_sha | default('unknown') }}"
deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}"
app_stack_path: "{{ deploy_user_home }}/deployment/stacks/application"
pre_tasks:
- name: Optionally load registry credentials from encrypted vault
@@ -29,12 +30,21 @@
name: docker
state: started
register: docker_service
become: yes
- name: Fail if Docker is not running
fail:
msg: "Docker service is not running"
when: docker_service.status.ActiveState != 'active'
- name: Ensure application stack directory exists
file:
path: "{{ app_stack_path }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0755'
- name: Create backup directory
file:
path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}"
@@ -46,16 +56,15 @@
tasks:
- name: Backup current deployment metadata
shell: |
docker service inspect {{ stack_name }}_app --format '{{ "{{" }}.Spec.TaskTemplate.ContainerSpec.Image{{ "}}" }}' \
> {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_image.txt || true
docker stack ps {{ stack_name }} --format 'table {{ "{{" }}.Name{{ "}}" }}\t{{ "{{" }}.Image{{ "}}" }}\t{{ "{{" }}.CurrentState{{ "}}" }}' \
> {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/stack_status.txt || true
docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true
docker compose -f {{ app_stack_path }}/docker-compose.yml config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true
args:
executable: /bin/bash
changed_when: false
ignore_errors: yes
- name: Login to Docker registry (if credentials provided)
docker_login:
community.docker.docker_login:
registry_url: "{{ docker_registry_url }}"
username: "{{ docker_registry_username }}"
password: "{{ docker_registry_password }}"
@@ -65,43 +74,55 @@
- docker_registry_password is defined
- name: Pull new Docker image
docker_image:
community.docker.docker_image:
name: "{{ app_image }}"
tag: "{{ image_tag }}"
source: pull
force_source: yes
register: image_pull
- name: Update docker-compose.prod.yml with new image tag
lineinfile:
path: "{{ compose_file }}"
- name: Verify image was pulled successfully
fail:
msg: "Failed to pull image {{ app_image }}:{{ image_tag }}"
when: image_pull.failed
- name: Update docker-compose.yml with new image tag (all services)
replace:
path: "{{ app_stack_path }}/docker-compose.yml"
regexp: '^(\s+image:\s+){{ app_image }}:.*$'
line: '\1{{ app_image }}:{{ image_tag }}'
backrefs: yes
when: compose_file is file
replace: '\1{{ app_image }}:{{ image_tag }}'
when: image_pull.changed or image_tag != 'latest'
register: compose_updated
- name: Deploy stack update
docker_stack:
name: "{{ stack_name }}"
compose:
- "{{ compose_file }}"
- name: Restart application stack with new image
community.docker.docker_compose_v2:
project_src: "{{ app_stack_path }}"
state: present
prune: yes
pull: always
recreate: always
remove_orphans: yes
register: stack_deploy
when: image_pull.changed or compose_updated.changed
- name: Wait for service to be updated
command: >
docker service ps {{ stack_name }}_app
--filter "desired-state=running"
--format '{{ "{{" }}.CurrentState{{ "}}" }}'
register: service_status
until: "'Running' in service_status.stdout"
retries: 30
delay: 10
- name: Wait for services to be healthy
wait_for:
timeout: 60
changed_when: false
- name: Get updated service info
command: docker service inspect {{ stack_name }}_app --format '{{ "{{" }}.Spec.TaskTemplate.ContainerSpec.Image{{ "}}" }}'
- name: Check container health status
shell: |
docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json | jq -r '.[] | select(.Health != "healthy" and .Health != "") | "\(.Name): \(.Health)"' || echo "All healthy or no health checks"
args:
executable: /bin/bash
register: health_status
changed_when: false
ignore_errors: yes
?? - name: Get deployed image information
shell: |
docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
args:
executable: /bin/bash
register: deployed_image
changed_when: false
@@ -112,7 +133,9 @@
Git Commit: {{ git_commit_sha }}
Image Tag: {{ image_tag }}
Deployed Image: {{ deployed_image.stdout }}
Stack Deploy Output: {{ stack_deploy }}
Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }}
Stack Deploy: {{ 'UPDATED' if stack_deploy.changed else 'NO_CHANGE' }}
Health Status: {{ health_status.stdout if health_status.stdout != '' else 'All services healthy' }}
dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
@@ -121,17 +144,22 @@
- name: Cleanup old backups (keep last {{ max_rollback_versions }})
shell: |
cd {{ backups_path }}
ls -t | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf
ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf
args:
executable: /bin/bash
changed_when: false
ignore_errors: yes
post_tasks:
- name: Display deployment summary
debug:
msg:
- "Deployment completed successfully!"
- "=== Deployment Summary ==="
- "Image: {{ app_image }}:{{ image_tag }}"
- "Commit: {{ git_commit_sha }}"
- "Timestamp: {{ deployment_timestamp }}"
- "Health check URL: {{ health_check_url }}"
- "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}"
- "Stack Deploy: {{ 'UPDATED' if stack_deploy.changed else 'NO_CHANGE' }}"
- "Health Check URL: {{ health_check_url }}"
- ""
- "Next: Verify application is healthy"

View File

@@ -0,0 +1,166 @@
---
- name: Rollback Application Deployment
hosts: production
gather_facts: yes
become: no
vars:
rollback_to_version: "{{ rollback_to_version | default('previous') }}"
app_stack_path: "{{ deploy_user_home }}/deployment/stacks/application"
pre_tasks:
- name: Optionally load registry credentials from encrypted vault
include_vars:
file: "{{ playbook_dir }}/../secrets/production.vault.yml"
no_log: yes
ignore_errors: yes
delegate_to: localhost
become: no
- name: Derive docker registry credentials from vault when not provided
set_fact:
docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(omit)) }}"
docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(omit)) }}"
- name: Check Docker service
systemd:
name: docker
state: started
register: docker_service
become: yes
- name: Fail if Docker is not running
fail:
msg: "Docker service is not running - cannot perform rollback"
when: docker_service.status.ActiveState != 'active'
- name: Get list of available backups
find:
paths: "{{ backups_path }}"
file_type: directory
register: available_backups
- name: Fail if no backups available
fail:
msg: "No backup versions available for rollback"
when: available_backups.matched == 0
- name: Sort backups by date (newest first)
set_fact:
sorted_backups: "{{ available_backups.files | sort(attribute='mtime', reverse=true) }}"
tasks:
- name: Determine rollback target
set_fact:
rollback_backup: "{{ sorted_backups[1] if rollback_to_version == 'previous' else sorted_backups | selectattr('path', 'search', rollback_to_version) | first }}"
when: sorted_backups | length > 1
- name: Fail if rollback target not found
fail:
msg: "Cannot determine rollback target. Available backups: {{ sorted_backups | map(attribute='path') | list }}"
when: rollback_backup is not defined
- name: Load rollback metadata
slurp:
src: "{{ rollback_backup.path }}/deployment_metadata.txt"
register: rollback_metadata
ignore_errors: yes
- name: Parse rollback image from metadata
set_fact:
rollback_image: "{{ rollback_metadata.content | b64decode | regex_search('Deployed Image: ([^\\n]+)', '\\1') | first }}"
when: rollback_metadata is succeeded
- name: Alternative: Parse rollback image from docker-compose config backup
set_fact:
rollback_image: "{{ rollback_metadata.content | b64decode | regex_search('image:\\s+([^:]+):([^\\n]+)', '\\1:\\2') | first }}"
when:
- rollback_metadata is succeeded
- rollback_image is not defined
- name: Fail if cannot determine rollback image
fail:
msg: "Cannot determine image to rollback to from backup: {{ rollback_backup.path }}"
when: rollback_image is not defined or rollback_image == ''
- name: Display rollback information
debug:
msg:
- "Rolling back to previous version"
- "Backup: {{ rollback_backup.path }}"
- "Image: {{ rollback_image }}"
- name: Login to Docker registry (if credentials provided)
community.docker.docker_login:
registry_url: "{{ docker_registry_url }}"
username: "{{ docker_registry_username }}"
password: "{{ docker_registry_password }}"
no_log: yes
when:
- docker_registry_username is defined
- docker_registry_password is defined
- name: Pull rollback image
community.docker.docker_image:
name: "{{ rollback_image.split(':')[0] }}"
tag: "{{ rollback_image.split(':')[1] }}"
source: pull
force_source: yes
register: image_pull
- name: Update docker-compose.yml with rollback image
replace:
path: "{{ app_stack_path }}/docker-compose.yml"
regexp: '^(\s+image:\s+){{ app_image }}:.*$'
replace: '\1{{ rollback_image }}'
register: compose_updated
- name: Restart application stack with rollback image
community.docker.docker_compose_v2:
project_src: "{{ app_stack_path }}"
state: present
pull: always
recreate: always
remove_orphans: yes
register: stack_rollback
when: compose_updated.changed
- name: Wait for services to be healthy after rollback
wait_for:
timeout: 60
changed_when: false
- name: Get current running image
shell: |
docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown"
args:
executable: /bin/bash
register: current_image
changed_when: false
- name: Record rollback event
copy:
content: |
Rollback Timestamp: {{ ansible_date_time.iso8601 }}
Rolled back from: {{ sorted_backups[0].path | basename }}
Rolled back to: {{ rollback_backup.path | basename }}
Rollback Image: {{ rollback_image }}
Current Image: {{ current_image.stdout }}
dest: "{{ backups_path }}/rollback_{{ ansible_date_time.epoch }}.txt"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0644'
post_tasks:
- name: Display rollback summary
debug:
msg:
- "✅ Rollback completed successfully!"
- "Rolled back to: {{ rollback_image }}"
- "From backup: {{ rollback_backup.path }}"
- "Current running image: {{ current_image.stdout }}"
- "Health check URL: {{ health_check_url }}"
- name: Recommend health check
debug:
msg: "⚠️ Please verify application health at {{ health_check_url }}"

View File

@@ -0,0 +1,210 @@
---
- name: Deploy Infrastructure Stacks on Production Server
hosts: production
become: no
gather_facts: yes
vars:
stacks_base_path: "~/deployment/stacks"
wait_timeout: 60
tasks:
- name: Check if deployment stacks directory exists
stat:
path: "{{ stacks_base_path }}"
register: stacks_dir
- name: Fail if stacks directory doesn't exist
fail:
msg: "Deployment stacks directory not found at {{ stacks_base_path }}"
when: not stacks_dir.stat.exists
# Create external network required by all stacks
- name: Create traefik-public network
community.docker.docker_network:
name: traefik-public
driver: bridge
state: present
# 1. Deploy Traefik (Reverse Proxy & SSL)
- name: Deploy Traefik stack
community.docker.docker_compose_v2:
project_src: "{{ stacks_base_path }}/traefik"
state: present
pull: always
register: traefik_output
- name: Wait for Traefik to be ready
wait_for:
timeout: "{{ wait_timeout }}"
when: traefik_output.changed
- name: Check Traefik logs for readiness hint
shell: docker compose logs traefik 2>&1 | grep -Ei "(configuration loaded|traefik[[:space:]]v|Starting provider|Server configuration loaded)" || true
args:
chdir: "{{ stacks_base_path }}/traefik"
register: traefik_logs
until: traefik_logs.stdout != ""
retries: 6
delay: 10
changed_when: false
ignore_errors: yes
# 2. Deploy PostgreSQL (Database)
- name: Deploy PostgreSQL stack
community.docker.docker_compose_v2:
project_src: "{{ stacks_base_path }}/postgresql"
state: present
pull: always
register: postgres_output
- name: Wait for PostgreSQL to be ready
wait_for:
timeout: "{{ wait_timeout }}"
when: postgres_output.changed
- name: Check PostgreSQL logs for readiness
shell: docker compose logs postgres 2>&1 | grep -Ei "(ready to accept connections|database system is ready)" || true
args:
chdir: "{{ stacks_base_path }}/postgresql"
register: postgres_logs
until: postgres_logs.stdout != ""
retries: 6
delay: 10
changed_when: false
ignore_errors: yes
# 3. Deploy Docker Registry (Private Registry)
- name: Ensure Registry auth directory exists
file:
path: "{{ stacks_base_path }}/registry/auth"
state: directory
mode: '0755'
become: yes
- name: Create Registry htpasswd file if missing
shell: |
if [ ! -f {{ stacks_base_path }}/registry/auth/htpasswd ]; then
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin registry-secure-password-2025 > {{ stacks_base_path }}/registry/auth/htpasswd
chmod 644 {{ stacks_base_path }}/registry/auth/htpasswd
fi
args:
executable: /bin/bash
become: yes
changed_when: true
register: registry_auth_created
- name: Deploy Docker Registry stack
community.docker.docker_compose_v2:
project_src: "{{ stacks_base_path }}/registry"
state: present
pull: always
register: registry_output
- name: Wait for Docker Registry to be ready
wait_for:
timeout: "{{ wait_timeout }}"
when: registry_output.changed
- name: Check Registry logs for readiness
shell: docker compose logs registry 2>&1 | grep -Ei "(listening on|listening at|http server)" || true
args:
chdir: "{{ stacks_base_path }}/registry"
register: registry_logs
until: registry_logs.stdout != ""
retries: 6
delay: 10
changed_when: false
ignore_errors: yes
- name: Verify Registry is accessible
uri:
url: "http://127.0.0.1:5000/v2/_catalog"
user: admin
password: registry-secure-password-2025
status_code: 200
timeout: 5
register: registry_check
ignore_errors: yes
changed_when: false
- name: Display Registry status
debug:
msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}"
# 4. Deploy Gitea (CRITICAL - Git Server + MySQL + Redis)
- name: Deploy Gitea stack
community.docker.docker_compose_v2:
project_src: "{{ stacks_base_path }}/gitea"
state: present
pull: always
register: gitea_output
- name: Wait for Gitea to be ready
wait_for:
timeout: "{{ wait_timeout }}"
when: gitea_output.changed
- name: Check Gitea logs for readiness
shell: docker compose logs gitea 2>&1 | grep -Ei "(Listen:|Server is running|Starting server)" || true
args:
chdir: "{{ stacks_base_path }}/gitea"
register: gitea_logs
until: gitea_logs.stdout != ""
retries: 12
delay: 10
changed_when: false
ignore_errors: yes
# 5. Deploy Monitoring (Portainer + Grafana + Prometheus)
- name: Deploy Monitoring stack
community.docker.docker_compose_v2:
project_src: "{{ stacks_base_path }}/monitoring"
state: present
pull: always
register: monitoring_output
- name: Wait for Monitoring to be ready
wait_for:
timeout: "{{ wait_timeout }}"
when: monitoring_output.changed
# Verification
- name: List all running containers
command: >
docker ps --format 'table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}'
register: docker_ps_output
- name: Display running containers
debug:
msg: "{{ docker_ps_output.stdout_lines }}"
- name: Verify Gitea accessibility via HTTPS
uri:
url: https://git.michaelschiemer.de
method: GET
validate_certs: no
status_code: 200
timeout: 10
register: gitea_http_check
ignore_errors: yes
- name: Display Gitea accessibility status
debug:
msg: "Gitea HTTPS check: {{ 'SUCCESS' if gitea_http_check.status == 200 else 'FAILED - Status: ' + (gitea_http_check.status|string) }}"
- name: Summary
debug:
msg:
- "=== Infrastructure Deployment Complete ==="
- "Traefik: {{ 'Deployed' if traefik_output.changed else 'Already running' }}"
- "PostgreSQL: {{ 'Deployed' if postgres_output.changed else 'Already running' }}"
- "Docker Registry: {{ 'Deployed' if registry_output.changed else 'Already running' }}"
- "Gitea: {{ 'Deployed' if gitea_output.changed else 'Already running' }}"
- "Monitoring: {{ 'Deployed' if monitoring_output.changed else 'Already running' }}"
- ""
- "Next Steps:"
- "1. Access Gitea at: https://git.michaelschiemer.de"
- "2. Complete Gitea setup wizard if first-time deployment"
- "3. Navigate to Admin > Actions > Runners to get registration token"
- "4. Continue with Phase 1 - Gitea Runner Setup"

View File

@@ -0,0 +1,92 @@
---
- name: Setup Production Secrets
hosts: production
gather_facts: yes
become: yes
vars:
vault_file: "{{ playbook_dir }}/../secrets/production.vault.yml"
pre_tasks:
- name: Verify vault file exists
stat:
path: "{{ vault_file }}"
register: vault_stat
delegate_to: localhost
become: no
- name: Fail if vault file missing
fail:
msg: "Vault file not found at {{ vault_file }}"
when: not vault_stat.stat.exists
tasks:
- name: Detect Docker Swarm mode
shell: docker info -f '{{ "{{" }}.Swarm.LocalNodeState{{ "}}" }}'
register: swarm_state
changed_when: false
- name: Set fact if swarm is active
set_fact:
swarm_active: "{{ swarm_state.stdout | lower == 'active' }}"
- name: Load encrypted secrets
include_vars:
file: "{{ vault_file }}"
no_log: yes
- name: Ensure secrets directory exists
file:
path: "{{ secrets_path }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0700'
- name: Create .env.production file
template:
src: "{{ playbook_dir }}/../templates/.env.production.j2"
dest: "{{ secrets_path }}/.env.production"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
no_log: yes
- name: Create Docker secrets from vault (disabled for compose-only deployment)
docker_secret:
name: "{{ item.name }}"
data: "{{ item.value }}"
state: present
loop:
- name: db_password
value: "{{ vault_db_password }}"
- name: redis_password
value: "{{ vault_redis_password }}"
- name: app_key
value: "{{ vault_app_key }}"
- name: jwt_secret
value: "{{ vault_jwt_secret }}"
- name: mail_password
value: "{{ vault_mail_password }}"
no_log: yes
when: false
- name: Set secure permissions on secrets directory
file:
path: "{{ secrets_path }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0700'
recurse: yes
- name: Verify Docker secrets (skipped)
command: docker secret ls --format '{{ "{{" }}.Name{{ "}}" }}'
register: docker_secrets
changed_when: false
when: false
- name: Display deployed Docker secrets (skipped)
debug:
msg: "Deployed secrets: {{ docker_secrets.stdout_lines | default([]) }}"
when: false

View File

@@ -0,0 +1,86 @@
---
- name: Setup Let's Encrypt SSL Certificates via Traefik
hosts: production
become: no
gather_facts: yes
vars:
domains:
- git.michaelschiemer.de
- michaelschiemer.de
acme_email: kontakt@michaelschiemer.de
tasks:
- name: Check if acme.json exists and is a file
stat:
path: "{{ deploy_user_home }}/de iployment/stacks/traefik/acme.json"
register: acme_stat
- name: Remove acme.json if it's a directory
file:
path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json"
state: absent
become: yes
when: acme_stat.stat.exists and acme_stat.stat.isdir
- name: Ensure Traefik acme.json exists and has correct permissions
file:
path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json"
state: touch
mode: '0600'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
become: yes
- name: Verify Traefik is running
command: docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml ps traefik
register: traefik_status
changed_when: false
- name: Fail if Traefik is not running
fail:
msg: "Traefik is not running. Please start it first."
when: traefik_status.rc != 0 or "Up" not in traefik_status.stdout
- name: Force Traefik to reload configuration
command: docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml restart traefik
changed_when: true
- name: Wait for Traefik to be ready
wait_for:
timeout: 10
changed_when: false
- name: Trigger certificate request by accessing each domain
uri:
url: "https://{{ item }}"
method: GET
validate_certs: no
timeout: 5
status_code: [200, 301, 302, 303, 404, 502, 503]
loop: "{{ domains }}"
register: certificate_trigger
changed_when: false
ignore_errors: yes
- name: Wait for ACME certificate generation (30 seconds)
wait_for:
timeout: 30
changed_when: false
- name: Check if acme.json contains certificates
stat:
path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json"
register: acme_file
- name: Display certificate status
debug:
msg: |
Certificate setup triggered.
Traefik will request Let's Encrypt certificates for:
{{ domains | join(', ') }}
Check Traefik logs to see certificate generation progress:
docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml logs traefik | grep -i acme
Certificates should be ready within 1-2 minutes.

View File

@@ -0,0 +1,53 @@
---
- name: Sync Infrastructure Stacks to Production Server
hosts: production
become: no
gather_facts: yes
vars:
local_stacks_path: "/home/michael/dev/michaelschiemer/deployment/stacks"
remote_stacks_path: "~/deployment"
tasks:
- name: Ensure deployment directory exists on production
file:
path: "{{ remote_stacks_path }}"
state: directory
mode: '0755'
- name: Sync stacks directory to production server
synchronize:
src: "{{ local_stacks_path }}"
dest: "{{ remote_stacks_path }}/"
delete: no
recursive: yes
rsync_opts:
- "--chmod=D755,F644"
- "--exclude=.git"
- "--exclude=*.log"
- "--exclude=data/"
- "--exclude=volumes/"
- name: Ensure executable permissions on PostgreSQL backup scripts
file:
path: "{{ item }}"
mode: '0755'
loop:
- "{{ remote_stacks_path }}/stacks/postgresql/scripts/backup-entrypoint.sh"
- "{{ remote_stacks_path }}/stacks/postgresql/scripts/backup.sh"
- "{{ remote_stacks_path }}/stacks/postgresql/scripts/restore.sh"
ignore_errors: yes
- name: Verify stacks directory exists on production
stat:
path: "{{ remote_stacks_path }}/stacks"
register: stacks_dir
- name: Display sync results
debug:
msg:
- "=== Stacks Synchronization Complete ==="
- "Stacks directory exists: {{ stacks_dir.stat.exists }}"
- "Path: {{ remote_stacks_path }}/stacks"
- ""
- "Next: Run infrastructure deployment playbook"

View File

@@ -1,83 +0,0 @@
#!/bin/bash
# Production Deployment Script
# This script prepares the application for production deployment
set -e
echo "🚀 Starting Production Deployment..."
# Check if we're in the right directory
if [ ! -f "composer.json" ]; then
echo "❌ Error: Must be run from project root directory"
exit 1
fi
# Backup current .env if it exists
if [ -f ".env" ]; then
echo "📦 Backing up current .env to .env.backup"
cp .env .env.backup
fi
# Copy production environment file
echo "📝 Setting up production environment..."
cp .env.production .env
# Clear all caches
echo "🧹 Clearing caches..."
rm -rf storage/cache/*
rm -rf var/cache/*
rm -rf cache/*
# Install production dependencies (no dev dependencies)
echo "📦 Installing production dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
# Build production assets
echo "🎨 Building production assets..."
npm run build
# Set correct permissions
echo "🔐 Setting correct permissions..."
chmod -R 755 storage/
chmod -R 755 var/
chmod -R 755 public/
# Create necessary directories
mkdir -p storage/logs
mkdir -p storage/cache
mkdir -p var/cache
mkdir -p var/logs
# Run database migrations
echo "🗄️ Running database migrations..."
php console.php db:migrate --force
# Clear PHP opcache if available
if command -v cachetool &> /dev/null; then
echo "🔄 Clearing PHP opcache..."
cachetool opcache:reset
fi
# Restart services (if using systemctl)
if command -v systemctl &> /dev/null; then
echo "🔄 Restarting services..."
sudo systemctl restart php8.4-fpm
sudo systemctl restart nginx
fi
echo "✅ Production deployment complete!"
echo ""
echo "⚠️ IMPORTANT REMINDERS:"
echo "1. Ensure APP_ENV=production in .env"
echo "2. Ensure APP_DEBUG=false in .env"
echo "3. Update database credentials if needed"
echo "4. Update ADMIN_ALLOWED_IPS in .env for admin access"
echo "5. Test the site to ensure everything works"
echo ""
echo "🔒 Security Checklist:"
echo "[ ] Performance debug is disabled"
echo "[ ] Session debug info is hidden"
echo "[ ] Admin routes are IP-restricted"
echo "[ ] Error messages are generic"
echo "[ ] HTTPS is enforced"

View File

@@ -221,4 +221,4 @@ try {
exit(1);
}
echo "\n🎉 Migration script completed!\n";
echo "\n🎉 Migration script completed!\n";

34
scripts/prepare-secrets.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
# Helper Script to prepare secrets for Gitea manual setup
# Usage: ./scripts/prepare-secrets.sh
set -euo pipefail
echo "=== Gitea Secrets - Werte zum Kopieren ==="
echo ""
echo "1. REGISTRY_USER:"
echo "─────────────────"
echo "admin"
echo ""
echo ""
echo "2. REGISTRY_PASSWORD:"
echo "─────────────────────"
echo "registry-secure-password-2025"
echo ""
echo ""
echo "3. SSH_PRIVATE_KEY:"
echo "───────────────────"
if [ -f ~/.ssh/production ]; then
cat ~/.ssh/production
else
echo "⚠️ FEHLER: ~/.ssh/production nicht gefunden!"
echo "Bitte SSH Key erstellen oder Pfad anpassen."
exit 1
fi
echo ""
echo ""
echo "=== Nächste Schritte ==="
echo "1. Gehe zu: https://git.michaelschiemer.de/<username>/michaelschiemer/settings/secrets"
echo "2. Füge jedes Secret oben einzeln hinzu"
echo "3. Kopiere die Werte von oben für jedes Secret"
echo ""

View File

@@ -1,446 +0,0 @@
#!/bin/bash
# Production Deployment Script for Custom PHP Framework
# Comprehensive deployment automation with zero-downtime strategy
#
# Usage:
# ./scripts/production-deploy.sh [initial|update|rollback]
#
# Modes:
# initial - First-time production deployment
# update - Rolling update with zero downtime
# rollback - Rollback to previous version
set -euo pipefail
# Configuration
DEPLOY_MODE="${1:-update}"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BACKUP_DIR="${PROJECT_ROOT}/../backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_PATH="${BACKUP_DIR}/backup_${TIMESTAMP}"
# Colors
GREEN="\e[32m"
YELLOW="\e[33m"
RED="\e[31m"
BLUE="\e[34m"
RESET="\e[0m"
# Logging functions
log() {
echo -e "${BLUE}[$(date +'%H:%M:%S')]${RESET} $1"
}
success() {
echo -e "${GREEN}$1${RESET}"
}
warning() {
echo -e "${YELLOW}⚠️ $1${RESET}"
}
error() {
echo -e "${RED}$1${RESET}"
cleanup_on_error
exit 1
}
# Cleanup on error
cleanup_on_error() {
log "Cleaning up after error..."
if [[ -d "$BACKUP_PATH" ]]; then
warning "Rolling back to previous version..."
restore_backup "$BACKUP_PATH"
fi
}
# Prerequisites check
check_prerequisites() {
log "Checking prerequisites..."
# Check if running from project root
if [[ ! -f "$PROJECT_ROOT/composer.json" ]]; then
error "Must be run from project root directory"
fi
# Check Docker
if ! command -v docker &> /dev/null; then
error "Docker is not installed"
fi
# Check Docker Compose
if ! docker compose version &> /dev/null; then
error "Docker Compose is not installed"
fi
# Check .env.production exists
if [[ ! -f "$PROJECT_ROOT/.env.production" ]]; then
error ".env.production not found - copy from .env.example and configure"
fi
# Check docker-compose.production.yml exists
if [[ ! -f "$PROJECT_ROOT/docker-compose.production.yml" ]]; then
error "docker-compose.production.yml not found"
fi
# Verify VAULT_ENCRYPTION_KEY is set
if ! grep -q "VAULT_ENCRYPTION_KEY=" "$PROJECT_ROOT/.env.production" || \
grep -q "VAULT_ENCRYPTION_KEY=CHANGE_ME" "$PROJECT_ROOT/.env.production"; then
error "VAULT_ENCRYPTION_KEY not configured in .env.production"
fi
success "Prerequisites check passed"
}
# Create backup
create_backup() {
log "Creating backup..."
mkdir -p "$BACKUP_DIR"
# Backup database
if docker compose ps db | grep -q "Up"; then
log "Backing up database..."
docker compose exec -T db pg_dump -U postgres michaelschiemer_prod | \
gzip > "${BACKUP_PATH}_database.sql.gz"
success "Database backup created"
fi
# Backup .env
if [[ -f "$PROJECT_ROOT/.env" ]]; then
cp "$PROJECT_ROOT/.env" "${BACKUP_PATH}_env"
success ".env backup created"
fi
# Backup docker volumes (important directories)
if [[ -d "$PROJECT_ROOT/storage" ]]; then
tar -czf "${BACKUP_PATH}_storage.tar.gz" -C "$PROJECT_ROOT" storage
success "Storage backup created"
fi
success "Backup completed: $BACKUP_PATH"
}
# Restore from backup
restore_backup() {
local backup_path="$1"
log "Restoring from backup: $backup_path"
# Restore database
if [[ -f "${backup_path}_database.sql.gz" ]]; then
log "Restoring database..."
gunzip -c "${backup_path}_database.sql.gz" | \
docker compose exec -T db psql -U postgres michaelschiemer_prod
success "Database restored"
fi
# Restore .env
if [[ -f "${backup_path}_env" ]]; then
cp "${backup_path}_env" "$PROJECT_ROOT/.env"
success ".env restored"
fi
# Restore storage
if [[ -f "${backup_path}_storage.tar.gz" ]]; then
tar -xzf "${backup_path}_storage.tar.gz" -C "$PROJECT_ROOT"
success "Storage restored"
fi
# Restart services
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
restart
success "Backup restored successfully"
}
# Build Docker images
build_images() {
log "Building Docker images..."
cd "$PROJECT_ROOT"
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
build --no-cache
success "Docker images built"
}
# Run database migrations
run_migrations() {
log "Running database migrations..."
cd "$PROJECT_ROOT"
# Check migration status first
docker compose exec -T php php console.php db:status || true
# Run migrations
if ! docker compose exec -T php php console.php db:migrate; then
error "Database migrations failed"
fi
success "Database migrations completed"
}
# Initialize SSL certificates
init_ssl() {
log "Initializing SSL certificates..."
cd "$PROJECT_ROOT"
# Check if SSL is enabled
if grep -q "SSL_ENABLED=true" .env.production; then
log "SSL is enabled, checking certificate status..."
# Check certificate status
if docker compose exec -T php php console.php ssl:status 2>/dev/null | grep -q "Certificate is valid"; then
success "SSL certificate already exists and is valid"
else
warning "SSL certificate not found or invalid, initializing..."
if ! docker compose exec -T php php console.php ssl:init; then
error "SSL initialization failed"
fi
success "SSL certificate initialized"
fi
else
warning "SSL is disabled in .env.production"
fi
}
# Verify Vault configuration
verify_vault() {
log "Verifying Vault configuration..."
cd "$PROJECT_ROOT"
# Test Vault access
if ! docker compose exec -T php php console.php vault:list &>/dev/null; then
error "Vault not accessible - check VAULT_ENCRYPTION_KEY"
fi
success "Vault is configured correctly"
}
# Health check with retries
health_check() {
local max_retries=30
local retry_count=0
log "Running health checks..."
while [[ $retry_count -lt $max_retries ]]; do
if curl -f -s -k -H "User-Agent: Mozilla/5.0 (Deployment Health Check)" "https://localhost/health" > /dev/null 2>&1; then
success "Health check passed"
return 0
fi
retry_count=$((retry_count + 1))
log "Health check attempt $retry_count/$max_retries..."
sleep 2
done
error "Health check failed after $max_retries attempts"
}
# Initial deployment
initial_deployment() {
log "🚀 Starting initial production deployment..."
check_prerequisites
cd "$PROJECT_ROOT"
# 1. Generate Vault encryption key if not exists
if grep -q "VAULT_ENCRYPTION_KEY=CHANGE_ME" .env.production; then
log "Generating Vault encryption key..."
warning "Make sure to backup this key securely!"
# Key generation is done manually for security
error "Please generate VAULT_ENCRYPTION_KEY with: docker exec php php console.php vault:generate-key"
fi
# 2. Build images
build_images
# 3. Start services
log "Starting Docker services..."
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
up -d
# 4. Wait for services to be ready
log "Waiting for services to be ready..."
sleep 20
# 5. Run migrations
run_migrations
# 6. Initialize SSL
init_ssl
# 7. Verify Vault
verify_vault
# 8. Health check
health_check
# 9. Display summary
deployment_summary
success "🎉 Initial deployment completed successfully!"
}
# Update deployment (zero-downtime)
update_deployment() {
log "🔄 Starting rolling update deployment..."
check_prerequisites
create_backup
cd "$PROJECT_ROOT"
# 1. Pull latest images (if using registry)
log "Pulling latest images..."
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
pull || warning "Pull failed (not critical if building locally)"
# 2. Build new images
build_images
# 3. Run migrations (if any)
log "Running database migrations..."
docker compose exec -T php php console.php db:migrate || warning "No new migrations"
# 4. Rolling restart with health checks
log "Performing rolling restart..."
# Restart PHP-FPM first
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
up -d --no-deps --force-recreate php
sleep 10
# Restart web server
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
up -d --no-deps --force-recreate web
sleep 5
# Restart queue workers (graceful shutdown via stop_grace_period)
docker compose -f docker-compose.yml \
-f docker-compose.production.yml \
--env-file .env.production \
up -d --no-deps --force-recreate --scale queue-worker=2 queue-worker
# 5. Health check
health_check
# 6. Cleanup old images
log "Cleaning up old Docker images..."
docker image prune -f
# 7. Display summary
deployment_summary
success "🎉 Update deployment completed successfully!"
}
# Rollback deployment
rollback_deployment() {
log "⏪ Starting rollback..."
# Find latest backup
local latest_backup=$(find "$BACKUP_DIR" -name "backup_*_database.sql.gz" | sort -r | head -1)
if [[ -z "$latest_backup" ]]; then
error "No backup found for rollback"
fi
local backup_prefix="${latest_backup%_database.sql.gz}"
warning "Rolling back to: $backup_prefix"
read -p "Continue? (yes/no): " confirm
if [[ "$confirm" != "yes" ]]; then
log "Rollback cancelled"
exit 0
fi
restore_backup "$backup_prefix"
health_check
success "🎉 Rollback completed successfully!"
}
# Deployment summary
deployment_summary() {
echo ""
echo -e "${GREEN}========================================${RESET}"
echo -e "${GREEN} Deployment Summary${RESET}"
echo -e "${GREEN}========================================${RESET}"
echo ""
echo "📋 Mode: $DEPLOY_MODE"
echo "⏰ Timestamp: $(date)"
echo "📁 Project: $PROJECT_ROOT"
echo "💾 Backup: $BACKUP_PATH"
echo ""
echo "🐳 Docker Services:"
docker compose ps
echo ""
echo "🔒 Security Checks:"
echo " [ ] APP_ENV=production in .env.production"
echo " [ ] APP_DEBUG=false in .env.production"
echo " [ ] VAULT_ENCRYPTION_KEY configured"
echo " [ ] ADMIN_ALLOWED_IPS configured"
echo " [ ] SSL certificates valid"
echo ""
echo "📊 Health Check:"
echo " ✅ Application: https://localhost/health"
echo ""
echo "📝 Next Steps:"
echo " 1. Verify all services are running"
echo " 2. Check logs: docker compose logs -f --tail=100"
echo " 3. Test critical user flows"
echo " 4. Monitor error rates"
echo ""
echo -e "${GREEN}========================================${RESET}"
}
# Main deployment logic
main() {
case "$DEPLOY_MODE" in
initial)
initial_deployment
;;
update)
update_deployment
;;
rollback)
rollback_deployment
;;
*)
error "Invalid deployment mode: $DEPLOY_MODE. Use: initial|update|rollback"
;;
esac
}
# Trap errors
trap cleanup_on_error ERR
# Run main
main "$@"

View File

@@ -1,240 +0,0 @@
#!/bin/bash
# Server-side Deployment Script für Custom PHP Framework
# Läuft auf dem Production-Server (94.16.110.151)
set -euo pipefail # Exit on any error
DEPLOY_TAG="$1"
DEPLOY_PATH="$2"
BACKUP_PATH="${DEPLOY_PATH}-backup-$(date +%Y%m%d-%H%M%S)"
TEMP_PATH="${DEPLOY_PATH}-deploying"
# Farben
GREEN="\e[32m"
YELLOW="\e[33m"
RED="\e[31m"
BLUE="\e[34m"
RESET="\e[0m"
log() {
echo -e "${BLUE}[SERVER $(date +'%H:%M:%S')]${RESET} $1"
}
success() {
echo -e "${GREEN}$1${RESET}"
}
error() {
echo -e "${RED}$1${RESET}"
cleanup_on_error
exit 1
}
cleanup_on_error() {
log "Cleanup nach Fehler..."
[[ -d "$TEMP_PATH" ]] && rm -rf "$TEMP_PATH"
[[ -d "$BACKUP_PATH" ]] && {
log "Stelle vorherige Version wieder her..."
[[ -d "$DEPLOY_PATH" ]] && rm -rf "$DEPLOY_PATH"
mv "$BACKUP_PATH" "$DEPLOY_PATH"
cd "$DEPLOY_PATH" && docker compose restart
}
}
# Atomic Deployment mit Git
atomic_git_deployment() {
log "Starte atomic git deployment..."
# 1. Backup der aktuellen Version erstellen
if [[ -d "$DEPLOY_PATH" ]]; then
log "Erstelle Backup der aktuellen Version..."
cp -r "$DEPLOY_PATH" "$BACKUP_PATH"
success "Backup erstellt: $BACKUP_PATH"
fi
# 2. Neue Version in temporäres Verzeichnis clonen
log "Clone neue Version mit Tag: $DEPLOY_TAG"
if [[ -d "$TEMP_PATH" ]]; then
rm -rf "$TEMP_PATH"
fi
# Git Repository klonen oder pullen
if [[ -d "$DEPLOY_PATH/.git" ]]; then
# Existierendes Repository - fetch und checkout
log "Update existierendes Repository..."
cd "$DEPLOY_PATH"
git fetch --tags origin
git checkout "$DEPLOY_TAG"
success "Git checkout zu $DEPLOY_TAG erfolgreich"
else
# Neues Repository klonen
log "Klone Repository neu..."
git clone --depth 1 --branch "$DEPLOY_TAG" "$(git -C . remote get-url origin)" "$TEMP_PATH" || {
error "Git clone fehlgeschlagen"
}
# Atomic Switch: Temp → Live
if [[ -d "$DEPLOY_PATH" ]]; then
mv "$DEPLOY_PATH" "${DEPLOY_PATH}-old"
fi
mv "$TEMP_PATH" "$DEPLOY_PATH"
fi
}
# Framework-spezifische Deployment-Schritte
framework_deployment() {
log "Führe Framework-Deployment durch..."
cd "$DEPLOY_PATH"
# 1. Environment Setup
if [[ ! -f .env ]]; then
if [[ -f .env.production ]]; then
log "Kopiere .env.production zu .env"
cp .env.production .env
else
error ".env.production nicht gefunden"
fi
fi
# 2. Composer Dependencies (Production)
log "Installiere Composer Dependencies..."
if ! composer install --no-dev --optimize-autoloader --no-interaction; then
error "Composer Install fehlgeschlagen"
fi
# 3. Framework Cache warming (falls implementiert)
if [[ -f console.php ]] && php console.php list | grep -q "cache:warm"; then
log "Wärme Framework Caches auf..."
php console.php cache:warm || warning "Cache warming fehlgeschlagen"
fi
# 4. Database Migrations
log "Führe Database Migrations durch..."
if php console.php list | grep -q "db:migrate"; then
php console.php db:migrate || error "Database Migration fehlgeschlagen"
else
warning "Keine Database Migrations gefunden"
fi
# 5. Framework optimizations
if php console.php list | grep -q "framework:optimize"; then
log "Optimiere Framework..."
php console.php framework:optimize || warning "Framework Optimization fehlgeschlagen"
fi
success "Framework-Deployment abgeschlossen"
}
# Docker Services Management
manage_docker_services() {
log "Manage Docker Services..."
cd "$DEPLOY_PATH"
# Prüfe ob docker-compose.yml existiert
if [[ ! -f docker-compose.yml ]]; then
error "docker-compose.yml nicht gefunden"
fi
# Services neu starten mit minimaler Downtime
log "Starte Services neu..."
# Rolling Update Strategy
docker compose pull --quiet || warning "Docker Pull Warnings (nicht kritisch)"
# Restart mit Health Checks
if docker compose up -d --force-recreate --remove-orphans; then
success "Docker Services erfolgreich neu gestartet"
else
error "Docker Restart fehlgeschlagen"
fi
# Warte auf Service-Start
log "Warte auf Service-Start..."
sleep 10
}
# Health Checks
health_checks() {
log "Führe Health Checks durch..."
cd "$DEPLOY_PATH"
# 1. Framework Health Check
if php console.php health:check; then
success "Framework Health Check OK"
else
error "Framework Health Check fehlgeschlagen"
fi
# 2. Docker Services Check
if ! docker compose ps | grep -q "Up"; then
error "Docker Services nicht verfügbar"
fi
success "Docker Services OK"
# 3. MCP Server Test (optional)
if timeout 5 php console.php mcp:server --test 2>/dev/null; then
success "MCP Server OK"
else
log "MCP Server Test übersprungen (optional)"
fi
# 4. Web Response Test
sleep 5
if curl -f -s -H "User-Agent: Mozilla/5.0 (Deployment Health Check)" "http://localhost" > /dev/null; then
success "HTTP Response OK"
else
warning "HTTP Response Test fehlgeschlagen (möglicherweise nginx-Problem)"
fi
}
# Cleanup alte Backups (behalte nur die letzten 5)
cleanup_old_backups() {
log "Räume alte Backups auf..."
# Finde alle Backup-Ordner und behalte nur die 5 neuesten
find "$(dirname "$DEPLOY_PATH")" -maxdepth 1 -name "*-backup-*" -type d | \
sort -r | \
tail -n +6 | \
xargs -r rm -rf
success "Backup-Cleanup abgeschlossen"
}
# Deployment-Summary
deployment_summary() {
echo -e "${GREEN}"
echo "🎉 Server-Deployment erfolgreich!"
echo "📋 Tag: $DEPLOY_TAG"
echo "📁 Path: $DEPLOY_PATH"
echo "💾 Backup: $BACKUP_PATH"
echo "⏰ Zeit: $(date)"
echo -e "${RESET}"
}
# Hauptprogramm
main() {
log "🚀 Server-side Deployment gestartet"
log "📦 Tag: $DEPLOY_TAG"
log "📁 Path: $DEPLOY_PATH"
atomic_git_deployment
framework_deployment
manage_docker_services
health_checks
cleanup_old_backups
deployment_summary
# Cleanup erfolgreicher Deployment
[[ -d "${DEPLOY_PATH}-old" ]] && rm -rf "${DEPLOY_PATH}-old"
}
# Error Handling
trap cleanup_on_error ERR
# Script ausführen
main "$@"

View File

@@ -0,0 +1,123 @@
#!/bin/bash
# Interactive Script to set Gitea Repository Secrets via API
# Usage: ./scripts/setup-gitea-secrets-interactive.sh
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.michaelschiemer.de}"
REPO_OWNER="${REPO_OWNER:-michael}"
REPO_NAME="${REPO_NAME:-michaelschiemer}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== Gitea Repository Secrets Setup ===${NC}"
echo ""
echo "Repository: ${REPO_OWNER}/${REPO_NAME}"
echo "Gitea URL: ${GITEA_URL}"
echo ""
# Check for existing token
if [ -z "${GITEA_TOKEN:-}" ]; then
echo -e "${YELLOW}Gitea Access Token benötigt${NC}"
echo ""
echo "Bitte generiere einen Token:"
echo "1. Gehe zu: ${GITEA_URL}/user/settings/applications"
echo "2. Klicke 'Generate New Token'"
echo "3. Name: 'secrets-setup'"
echo "4. Scopes: 'write:repository' (oder alle)"
echo "5. Kopiere den Token"
echo ""
read -sp "Gitea Token: " GITEA_TOKEN
echo ""
echo ""
fi
if [ -z "${GITEA_TOKEN:-}" ]; then
echo -e "${RED}❌ Token erforderlich - Abbruch${NC}"
exit 1
fi
# Function to create/update secret via API
set_secret() {
local secret_name=$1
local secret_value=$2
echo -n "Setting $secret_name... "
# Gitea API endpoint: PUT /repos/{owner}/{repo}/actions/secrets/{secretname}
local response=$(curl -s -w "\n%{http_code}" \
-X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${secret_name}" \
-d "{
\"data\": \"$(printf '%s' "$secret_value" | base64 | tr -d '\n')\"
}" 2>&1)
local http_code=$(echo "$response" | tail -n1)
local body=$(echo "$response" | sed '$d')
if [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then
echo -e "${GREEN}✅ OK${NC}"
return 0
elif [ "$http_code" = "404" ]; then
echo -e "${YELLOW}⚠️ Repository oder Token-Berechtigung fehlt${NC}"
return 1
else
echo -e "${RED}❌ FAILED (HTTP $http_code)${NC}"
echo "Response: $body"
return 1
fi
}
# Get registry password (default)
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-registry-secure-password-2025}"
# Get SSH private key
if [ -f ~/.ssh/production ]; then
SSH_PRIVATE_KEY=$(cat ~/.ssh/production)
echo -e "${GREEN}✓ SSH private key gefunden${NC}"
else
echo -e "${RED}✗ SSH private key nicht gefunden in ~/.ssh/production${NC}"
exit 1
fi
echo ""
echo "Setting secrets for repository: ${REPO_OWNER}/${REPO_NAME}"
echo ""
# Test API connection first
echo -n "Testing API connection... "
test_response=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}")
if [ "$test_response" != "200" ]; then
者可 echo -e "${RED}❌ FAILED (HTTP $test_response)${NC}"
echo ""
echo "Mögliche Probleme:"
echo "- Token ungültig oder fehlende Berechtigungen"
echo "- Repository nicht gefunden: ${REPO_OWNER}/${REPO_NAME}"
echo "- Netzwerkproblem"
exit 1
fi
echo -e "${GREEN}✅ OK${NC}"
echo ""
# Set secrets
set_secret "REGISTRY_USER" "admin"
set_secret "REGISTRY_PASSWORD" "$REGISTRY_PASSWORD"
set_secret "SSH_PRIVATE_KEY" "$SSH_PRIVATE_KEY"
echo ""
echo -e "${GREEN}=== Secrets Setup Complete ===${NC}"
echo ""
echo "Prüfe Secrets in Gitea UI:"
echo "${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/settings/secrets/actions"
echo ""

View File

@@ -0,0 +1,120 @@
#!/bin/bash
# Set Gitea Repository Secrets with Token
# Usage: ./scripts/setup-gitea-secrets-with-token.sh <GITEA_TOKEN>
# or: GITEA_TOKEN=xxx ./scripts/setup-gitea-secrets-with-token.sh
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.michaelschiemer.de}"
REPO_OWNER="${REPO_OWNER:-michael}"
REPO_NAME="${REPO_NAME:-michaelschiemer}"
GITEA_TOKEN="${1:-${GITEA_TOKEN:-}}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== Gitea Repository Secrets Setup ===${NC}"
echo ""
echo "Repository: ${REPO_OWNER}/${REPO_NAME}"
echo "Gitea URL: ${GITEA_URL}"
echo ""
# Check if token is provided
if [ -z "$GITEA_TOKEN" ]; then
echo -e "${RED}❌ Fehler: GITEA_TOKEN nicht angegeben${NC}"
echo ""
echo "Verwendung:"
echo " $0 <GITEA_TOKEN>"
echo " oder:"
echo " GITEA_TOKEN=<token> $0"
echo ""
exit 1
fi
# Function to create/update secret via API
set_secret() {
local secret_name=$1
local secret_value=$2
echo -n "Setting $secret_name... "
# Base64 encode the secret value
local encoded_value=$(printf '%s' "$secret_value" | base64 | tr -d '\n')
# Gitea API endpoint: PUT /repos/{owner}/{repo}/actions/secrets/{secretname}
local response=$(curl -s -w "\n%{http_code}" \
-X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${secret_name}" \
-d "{
\"data\": \"${encoded_value}\"
}" 2>&1)
local http_code=$(echo "$response" | tail -n1)
local body=$(echo "$response" | sed '$d')
if [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then
echo -e "${GREEN}✅ OK${NC}"
return 0
elif [ "$http_code" = "404" ]; then
echo -e "${YELLOW}⚠️ Repository oder Token-Berechtigung fehlt${NC}"
echo "Response: $body"
return 1
else
echo -e "${RED}❌ FAILED (HTTP $http_code)${NC}"
echo "Response: $body"
return 1
fi
}
# Get registry password (default)
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-registry-secure-password-2025}"
# Get SSH private key
if [ -f ~/.ssh/production ]; then
SSH_PRIVATE_KEY=$(cat ~/.ssh/production)
echo -e "${GREEN}✓ SSH private key gefunden${NC}"
else
echo -e "${RED}✗ SSH private key nicht gefunden in ~/.ssh/production${NC}"
exit 1
fi
echo ""
echo "Testing API connection..."
# Test API connection first
test_response=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}")
if [ "$test_response" != "200" ]; then
echo -e "${RED}❌ API-Verbindung fehlgeschlagen (HTTP $test_response)${NC}"
echo ""
echo "Mögliche Probleme:"
echo "- Token ungültig oder fehlende Berechtigungen"
echo "- Repository nicht gefunden: ${REPO_OWNER}/${REPO_NAME}"
echo "- Netzwerkproblem"
exit 1
fi
echo -e "${GREEN}✅ API-Verbindung erfolgreich${NC}"
echo ""
echo "Setting secrets..."
echo ""
# Set secrets
set_secret "REGISTRY_USER" "admin"
set_secret "REGISTRY_PASSWORD" "$REGISTRY_PASSWORD"
set_secret "SSH_PRIVATE_KEY" "$SSH_PRIVATE_KEY"
echo ""
echo -e "${GREEN}=== Secrets Setup Complete ===${NC}"
echo ""
echo "Prüfe Secrets in Gitea UI:"
echo "${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/settings/secrets/actions"
echo ""

96
scripts/setup-gitea-secrets.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/bin/bash
# Helper Script to set Gitea Repository Secrets
# Usage: ./scripts/setup-gitea-secrets.sh [GITEA_TOKEN]
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.michaelschiemer.de}"
REPO_OWNER="${REPO_OWNER:-$(git config user.name || echo 'michael')}"
REPO_NAME="${REPO_NAME:-michaelschiemer}"
GITEA_TOKEN="${1:-${GITEA_TOKEN:-}}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== Gitea Repository Secrets Setup ===${NC}"
echo ""
# Check if token is provided
if [ -z "$GITEA_TOKEN" ]; then
echo -e "${YELLOW}⚠️ GITEA_TOKEN nicht gesetzt${NC}"
echo ""
echo "Bitte generiere einen Gitea Access Token:"
echo "1. Gehe zu: ${GITEA_URL}/user/settings/applications"
echo "2. Klicke 'Generate New Token'"
echo "3. Name: 'secrets-setup'"
echo "4. Scopes: 'write:repository'"
echo "5. Kopiere den Token"
echo ""
echo "Dann führe aus:"
echo " export GITEA_TOKEN='dein-token'"
echo " ./scripts/setup-gitea-secrets.sh"
echo ""
exit 1
fi
# Function to create/update secret
set_secret() {
local secret_name=$1
local secret_value=$2
echo -n "Setting $secret_name... "
# Gitea API endpoint for repository secrets
local response=$(curl -s -w "\n%{http_code}" \
-X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${secret_name}" \
-d "{
\"data\": \"${secret_value}\"
}")
local http_code=$(echo "$response" | tail -n1)
local body=$(echo "$response" | sed '$d')
if [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then
echo -e "${GREEN}✅ OK${NC}"
return 0
else
echo -e "${RED}❌ FAILED (HTTP $http_code)${NC}"
echo "Response: $body"
return 1
fi
}
# Get registry password (default or from vault)
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-registry-secure-password-2025}"
# Get SSH private key
if [ -f ~/.ssh/production ]; then
SSH_PRIVATE_KEY=$(cat ~/.ssh/production)
echo -e "${GREEN}✓ SSH private key gefunden${NC}"
else
echo -e "${RED}✗ SSH private key nicht gefunden in ~/.ssh/production${NC}"
echo "Bitte SSH key Pfad anpassen oder manuell setzen"
exit 1
fi
echo ""
echo "Setting secrets for repository: ${REPO_OWNER}/${REPO_NAME}"
echo ""
# Set secrets
set_secret "REGISTRY_USER" "admin"
set_secret "REGISTRY_PASSWORD" "$REGISTRY_PASSWORD"
set_secret "SSH_PRIVATE_KEY" "$SSH_PRIVATE_KEY"
echo ""
echo -e "${GREEN}=== Secrets Setup Complete ===${NC}"
echo ""
echo "Prüfe Secrets in Gitea UI:"
echo "${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/enu/repo/settings/secrets"
echo ""

View File

@@ -0,0 +1,85 @@
#!/bin/bash
# ==============================================================================
# Production Secrets Setup Script
# ==============================================================================
# This script creates Docker Secrets on the production server from .env values
# Run this ONCE during initial setup on the production server.
# ==============================================================================
set -e
echo "🔐 Docker Secrets Setup for Production"
echo "======================================"
echo ""
# Check if running on production server
if [ ! -f /home/deploy/framework/.env ]; then
echo "❌ ERROR: /home/deploy/framework/.env not found"
echo " Please ensure .env file exists on production server"
exit 1
fi
# Check if Docker Swarm is initialized
if ! docker info | grep -q "Swarm: active"; then
echo "❌ ERROR: Docker Swarm is not initialized"
echo " Run: docker swarm init"
exit 1
fi
echo "📋 Reading secrets from .env file..."
cd /home/deploy/framework
# Function to create secret from .env
create_secret() {
local secret_name=$1
local env_key=$2
# Extract value from .env
local value=$(grep "^${env_key}=" .env | cut -d'=' -f2- | sed 's/^"\(.*\)"$/\1/')
if [ -z "$value" ]; then
echo "⚠️ WARNING: ${env_key} not found in .env, skipping ${secret_name}"
return
fi
# Check if secret already exists
if docker secret ls --format "{{.Name}}" | grep -q "^${secret_name}$"; then
echo " Secret '${secret_name}' already exists, skipping..."
return
fi
# Create secret
echo "$value" | docker secret create "$secret_name" - 2>/dev/null
if [ $? -eq 0 ]; then
echo "✅ Created secret: ${secret_name}"
else
echo "❌ Failed to create secret: ${secret_name}"
fi
}
echo ""
echo "🔑 Creating Docker Secrets..."
echo ""
# Create all required secrets
create_secret "db_password" "DB_PASSWORD"
create_secret "app_key" "APP_KEY"
create_secret "vault_encryption_key" "VAULT_ENCRYPTION_KEY"
create_secret "shopify_webhook_secret" "SHOPIFY_WEBHOOK_SECRET"
create_secret "rapidmail_password" "RAPIDMAIL_PASSWORD"
echo ""
echo "📊 Verifying Secrets..."
echo ""
docker secret ls
echo ""
echo "✅ Secrets setup completed!"
echo ""
echo "Next steps:"
echo " 1. Deploy the stack: docker stack deploy -c docker-compose.prod.yml framework"
echo " 2. Monitor deployment: watch docker stack ps framework"
echo " 3. Check logs: docker service logs framework_web"
echo ""