ci: setup CI/CD pipeline with Gitea Actions and secrets configuration
This commit is contained in:
86
.gitea/workflows/AUTO_SETUP_SECRETS.md
Normal file
86
.gitea/workflows/AUTO_SETUP_SECRETS.md
Normal 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.
|
||||
90
.gitea/workflows/CI_CD_CHECKLIST.md
Normal file
90
.gitea/workflows/CI_CD_CHECKLIST.md
Normal 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"
|
||||
```
|
||||
101
.gitea/workflows/MANUAL_SETUP_SIMPLE.md
Normal file
101
.gitea/workflows/MANUAL_SETUP_SIMPLE.md
Normal 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.
|
||||
87
.gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md
Normal file
87
.gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md
Normal 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`
|
||||
119
.gitea/workflows/NEXT_STEPS.md
Normal file
119
.gitea/workflows/NEXT_STEPS.md
Normal 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
|
||||
104
.gitea/workflows/QUICK_SECRETS_SETUP.md
Normal file
104
.gitea/workflows/QUICK_SECRETS_SETUP.md
Normal 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
|
||||
```
|
||||
30
.gitea/workflows/REGISTRY_INFO.md
Normal file
30
.gitea/workflows/REGISTRY_INFO.md
Normal 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
|
||||
```
|
||||
86
.gitea/workflows/SECRETS_SETUP.md
Normal file
86
.gitea/workflows/SECRETS_SETUP.md
Normal 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
|
||||
132
.gitea/workflows/TEST_WORKFLOW.md
Normal file
132
.gitea/workflows/TEST_WORKFLOW.md
Normal 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
|
||||
75
.gitea/workflows/TOKEN_ISSUE_FIX.md
Normal file
75
.gitea/workflows/TOKEN_ISSUE_FIX.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
74
.gitea/workflows/update-production-secrets.yml
Normal file
74
.gitea/workflows/update-production-secrets.yml
Normal 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"
|
||||
@@ -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"
|
||||
|
||||
166
deployment/ansible/playbooks/rollback.yml
Normal file
166
deployment/ansible/playbooks/rollback.yml
Normal 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 }}"
|
||||
210
deployment/ansible/playbooks/setup-infrastructure.yml
Normal file
210
deployment/ansible/playbooks/setup-infrastructure.yml
Normal 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"
|
||||
92
deployment/ansible/playbooks/setup-production-secrets.yml
Normal file
92
deployment/ansible/playbooks/setup-production-secrets.yml
Normal 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
|
||||
86
deployment/ansible/playbooks/setup-ssl-certificates.yml
Normal file
86
deployment/ansible/playbooks/setup-ssl-certificates.yml
Normal 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.
|
||||
53
deployment/ansible/playbooks/sync-stacks.yml
Normal file
53
deployment/ansible/playbooks/sync-stacks.yml
Normal 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"
|
||||
@@ -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"
|
||||
34
scripts/prepare-secrets.sh
Executable file
34
scripts/prepare-secrets.sh
Executable 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 ""
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
123
scripts/setup-gitea-secrets-interactive.sh
Executable file
123
scripts/setup-gitea-secrets-interactive.sh
Executable 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 ""
|
||||
120
scripts/setup-gitea-secrets-with-token.sh
Executable file
120
scripts/setup-gitea-secrets-with-token.sh
Executable 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
96
scripts/setup-gitea-secrets.sh
Executable 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 ""
|
||||
85
scripts/setup-production-secrets.sh
Executable file
85
scripts/setup-production-secrets.sh
Executable 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 ""
|
||||
Reference in New Issue
Block a user