diff --git a/.gitea/workflows/build-image.yml b/.gitea/workflows/build-image.yml index 71a0966d..9fe9d6f5 100644 --- a/.gitea/workflows/build-image.yml +++ b/.gitea/workflows/build-image.yml @@ -150,7 +150,7 @@ jobs: NEEDS_BUILD=false OTHER_NON_IGNORED=false IGNORE_PATTERN='^(docs/|docs$|tests/|tests$|tests-e2e/|\.github/|\.gitea/|\.idea/|\.vscode/|\.husky/|.*\.md$|.*\.MD$|LICENSE$|CHANGELOG|CHANGELOG\.md$|\.editorconfig$|\.gitignore$)' - BUILD_TRIGGER_PATTERN='^(src/|resources/|config/|app/|public/|composer\.json$|composer\.lock$|composer/|package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|Dockerfile\.production$|Dockerfile\.runtime$|docker-compose\.production\.yml$|docker/|vite\.config\.(js|ts)$|tsconfig\.json$|babel\.config\.js$|Makefile$|artisan$)' + BUILD_TRIGGER_PATTERN='^(src/|resources/|config/|app/|public/|composer\.json$|composer\.lock$|composer/|package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|Dockerfile\.production$|Dockerfile\.runtime$|docker-compose\..*\.yml$|docker-compose\.yml$|docker/|vite\.config\.(js|ts)$|tsconfig\.json$|babel\.config\.js$|Makefile$|artisan$)' while IFS= read -r FILE; do [ -z "$FILE" ] && continue if echo "$FILE" | grep -Eq "$RUNTIME_PATTERN"; then @@ -817,10 +817,8 @@ jobs: # Job 3: Auto-deploy to Staging (only for staging branch) deploy-staging: name: Auto-deploy to Staging - needs: [changes, build, runtime-base] - if: | - (github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging'))) && - (needs.build.result != 'failure') + needs: [changes] + if: github.ref_name == 'staging' || github.head_ref == 'staging' || (github.ref_name == '' && contains(github.ref, 'staging')) runs-on: ubuntu-latest environment: name: staging @@ -876,28 +874,12 @@ jobs: DEPLOYMENT_HOST="${{ env.DEPLOYMENT_HOST }}" REGISTRY_HOST="${{ env.REGISTRY }}" IMAGE_NAME="${{ env.IMAGE_NAME }}" - BUILD_RESULT="${{ needs.build.result }}" - IMAGE_TAG_RAW="${{ needs.build.outputs.image_tag }}" - IMAGE_URL_RAW="${{ needs.build.outputs.image_url }}" DEFAULT_IMAGE="${REGISTRY_HOST}/${IMAGE_NAME}:latest" - SELECTED_IMAGE="" - if [ "$BUILD_RESULT" = "success" ] && [ -n "$IMAGE_URL_RAW" ] && [ "$IMAGE_URL_RAW" != "null" ]; then - SELECTED_IMAGE="$IMAGE_URL_RAW" - fi - - if [ -z "$SELECTED_IMAGE" ]; then - if [ "$BUILD_RESULT" = "success" ] && [ -n "$IMAGE_TAG_RAW" ] && [ "$IMAGE_TAG_RAW" != "null" ]; then - SELECTED_IMAGE="${REGISTRY_HOST}/${IMAGE_NAME}:${IMAGE_TAG_RAW}" - else - SELECTED_IMAGE="$DEFAULT_IMAGE" - fi - fi - - if [ -z "$SELECTED_IMAGE" ]; then - SELECTED_IMAGE="$DEFAULT_IMAGE" - fi + # Always use latest image - if a build happened, it would have pushed to latest anyway + # Using latest ensures we always get the most recent image, whether it was just built or not + SELECTED_IMAGE="$DEFAULT_IMAGE" STACK_PATH_DISPLAY="~/deployment/stacks/staging" @@ -1024,14 +1006,9 @@ jobs: echo "⏳ Waiting for services to start..." sleep 15 - # Pull latest code from Git repository only if image was actually rebuilt - # Skip if build was skipped (no changes detected) - container already has latest code - if [ "${{ needs.build.result }}" = "success" ] && [ -n "${{ needs.build.outputs.image_url }}" ] && [ "${{ needs.build.outputs.image_url }}" != "null" ]; then - echo "🔄 Pulling latest code from Git repository in staging-app container (image was rebuilt)..." - docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-app bash -c "cd /var/www/html && git -c safe.directory=/var/www/html fetch origin staging && git -c safe.directory=/var/www/html reset --hard origin/staging && git -c safe.directory=/var/www/html clean -fd" || echo "⚠️ Git pull failed, container will sync on next restart" - else - echo "ℹ️ Skipping Git pull - no new image built, container already has latest code" - fi + # Pull latest code from Git repository - always sync code when deploying + echo "🔄 Pulling latest code from Git repository in staging-app container..." + docker compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-app bash -c "cd /var/www/html && git -c safe.directory=/var/www/html fetch origin staging && git -c safe.directory=/var/www/html reset --hard origin/staging && git -c safe.directory=/var/www/html clean -fd" || echo "⚠️ Git pull failed, container will sync on next restart" # Also trigger a restart to ensure entrypoint script runs echo "🔄 Restarting staging-app to ensure all services are up-to-date..." diff --git a/deployment/stacks/semaphore/QUICKSTART.md b/deployment/stacks/semaphore/QUICKSTART.md new file mode 100644 index 00000000..eac75976 --- /dev/null +++ b/deployment/stacks/semaphore/QUICKSTART.md @@ -0,0 +1,201 @@ +# ?? Quick Start: Semaphore mit Repository verbinden + +Kurze Schritt-f?r-Schritt-Anleitung, um Semaphore mit deinem Git-Repository zu verbinden. + +## Schritt 1: Semaphore starten + +```bash +cd deployment/stacks/semaphore +cp env.example .env +``` + +**WICHTIG**: Generiere einen Encryption Key: +```bash +head -c32 /dev/urandom | base64 +``` +Kopiere den Key und setze ihn in `.env`: +```env +SEMAPHORE_ACCESS_KEY_ENCRYPTION= +``` + +```bash +docker compose up -d +``` + +## Schritt 2: Semaphore ?ffnen + +?ffne im Browser: **http://localhost:9300** + +**Login**: +- Username: `admin` +- Password: `admin` + +## Schritt 3: Projekt erstellen + +1. Klicke auf **"New Project"** oder **"Create Project"** +2. Name: **"michaelschiemer"** +3. Klicke auf **"Create"** + +## Schritt 4: Inventory erstellen (Hosts definieren) + +### 4.1 Inventory anlegen + +1. Gehe zu **Inventories** ? **New Inventory** +2. Name: **"Production Hosts"** +3. Klicke auf **"Create"** + +### 4.2 Host hinzuf?gen + +1. Klicke auf dein Inventory ? **"Add Host"** +2. F?lle aus: + +``` +Name: production +Address: 94.16.110.151 +SSH Username: deploy +SSH Port: 22 +``` + +3. Klicke auf **"Save"** + +### 4.3 SSH-Key hinzuf?gen + +1. Gehe zu **Keys** (in der Seitenleiste) +2. Klicke auf **"New Key"** +3. Name: **"Deployment Key"** +4. F?ge deinen SSH Private Key ein (aus `~/.ssh/production`): + ```bash + cat ~/.ssh/production + ``` +5. Klicke auf **"Save"** +6. Gehe zur?ck zu deinem Inventory und w?hle den Key bei deinem Host aus + +## Schritt 5: Template erstellen (Playbook verwenden) + +### 5.1 Template f?r CI Tests + +1. Gehe zu **Templates** ? **New Template** +2. **Name**: "Run CI Tests" +3. **Inventory**: W?hle "Production Hosts" +4. **Playbook Path**: `/tmp/semaphore/playbooks/ci-tests.yml` +5. **Variables** (klicke auf "Variables"): + ```yaml + repo_url: https://git.michaelschiemer.de/michael/michaelschiemer.git + repo_branch: main + ``` +6. Klicke auf **"Save"** + +### 5.2 Template f?r Docker Build + +1. **Templates** ? **New Template** +2. **Name**: "Build Docker Image" +3. **Inventory**: "Production Hosts" +4. **Playbook Path**: `/tmp/semaphore/playbooks/docker-build.yml` +5. **Variables**: + ```yaml + repo_url: https://git.michaelschiemer.de/michael/michaelschiemer.git + repo_branch: main + registry_url: registry.michaelschiemer.de + registry_user: admin + registry_password: + image_name: framework + image_tag: latest + ``` +6. **Save** + +### 5.3 Template f?r Staging Deployment + +1. **Templates** ? **New Template** +2. **Name**: "Deploy to Staging" +3. **Inventory**: "Production Hosts" +4. **Playbook Path**: `/tmp/semaphore/playbooks/deploy-staging.yml` +5. **Variables**: + ```yaml + registry_url: registry.michaelschiemer.de + registry_user: admin + registry_password: + image_name: framework + image_tag: latest + ``` +6. **Save** + +### 5.4 Template f?r Production Deployment + +1. **Templates** ? **New Template** +2. **Name**: "Deploy to Production" +3. **Inventory**: "Production Hosts" +4. **Playbook Path**: `/tmp/semaphore/playbooks/deploy-production.yml` +5. **Variables**: + ```yaml + registry_url: registry.michaelschiemer.de + registry_user: admin + registry_password: + image_name: framework + image_tag: latest + ``` +6. **Save** + +## Schritt 6: Task ausf?hren + +1. Gehe zu **Templates** +2. Klicke auf ein Template (z.B. "Run CI Tests") +3. Klicke auf **"Run"** +4. Beobachte die Ausf?hrung in Echtzeit + +## ? Fertig! + +Semaphore ist jetzt mit deinem Repository verbunden! Du kannst: + +- ? CI/CD-Tasks manuell starten +- ? Playbooks aus dem Repository verwenden +- ? Deployments auf deine Server ausf?hren + +## ?? Verf?gbare Playbooks + +Die folgenden Playbooks sind bereits im Repository und k?nnen in Semaphore verwendet werden: + +1. **`/tmp/semaphore/playbooks/ci-tests.yml`** - PHP Tests & Quality Checks +2. **`/tmp/semaphore/playbooks/docker-build.yml`** - Docker Image Build & Push +3. **`/tmp/semaphore/playbooks/deploy-staging.yml`** - Staging Deployment +4. **`/tmp/semaphore/playbooks/deploy-production.yml`** - Production Deployment + +## ?? Vorhandene Ansible-Playbooks + +Die vorhandenen Ansible-Playbooks aus `deployment/ansible/playbooks/` sind auch verf?gbar unter: +- `/tmp/semaphore/repo-playbooks/` + +Zum Beispiel: +- `/tmp/semaphore/repo-playbooks/deploy-update.yml` +- `/tmp/semaphore/repo-playbooks/rollback.yml` + +## ?? Tipps + +### Docker Socket f?r Build-Tasks + +Falls Docker-Build-Tasks ausgef?hrt werden sollen, f?ge den Docker Socket hinzu: + +In `docker-compose.yml` bei Semaphore-Service: +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro +``` + +Dann Semaphore neu starten: +```bash +docker compose restart semaphore +``` + +### Git-Integration + +Die Playbooks k?nnen direkt auf das Git-Repository zugreifen (HTTPS). F?r private Repositories oder SSH-Zugriff: + +1. Erstelle einen SSH-Key f?r Git in Semaphore (Keys ? New Key) +2. F?ge den Public Key zu deinem Git-Repository hinzu +3. Verwende SSH-URL in Playbook-Variablen: + ```yaml + repo_url: git@git.michaelschiemer.de:michael/michaelschiemer.git + ``` + +--- + +**Hilfe**: Weitere Details in `SETUP_REPOSITORY.md` und `README.md` diff --git a/deployment/stacks/semaphore/README.md b/deployment/stacks/semaphore/README.md index 230575e8..b74e04f2 100644 --- a/deployment/stacks/semaphore/README.md +++ b/deployment/stacks/semaphore/README.md @@ -6,9 +6,9 @@ Selbst-gehostete Semaphore CI/CD-Plattform für lokale Entwicklung, die es ermö **Features**: - **Selbst-gehostet**: Läuft vollständig lokal auf dem Entwicklungsrechner -- **Isoliert**: Keine externen Zugriffe, nur localhost (127.0.0.1) +- **Lokal isoliert**: NUR über localhost (127.0.0.1) erreichbar - KEIN externer Zugriff aus Sicherheitsgründen! - **MySQL-Backend**: Persistente Datenbank für Projekte, Tasks und Templates -- **Web-UI**: Intuitive Benutzeroberfläche für Pipeline-Management +- **Web-UI**: Intuitive Benutzeroberfläche für Pipeline-Management (nur lokal) - **Ansible-Integration**: Native Unterstützung für Ansible-Playbooks - **Docker-basiert**: Einfaches Setup und Wartung @@ -26,7 +26,7 @@ Selbst-gehostete Semaphore CI/CD-Plattform für lokale Entwicklung, die es ermö ## Voraussetzungen - Docker und Docker Compose installiert -- Port 3001 auf localhost frei verfügbar (3000 wird von Gitea verwendet) +- Port 9300 auf localhost frei verfügbar (3000 wird von Gitea verwendet) - Ausreichend Speicherplatz für Docker Volumes (~500MB initial) ## Verzeichnisstruktur @@ -75,7 +75,9 @@ docker compose up -d ### 4. Semaphore Web-UI öffnen -Öffne im Browser: http://localhost:3001 +Öffne im Browser: **http://localhost:9300** + +⚠️ **WICHTIG**: Semaphore ist NUR lokal zugänglich (127.0.0.1). Es gibt KEINEN externen Zugriff aus Sicherheitsgründen. **Standard-Login**: - **Username**: `admin` (oder Wert aus `SEMAPHORE_ADMIN`) @@ -104,8 +106,8 @@ MYSQL_PASSWORD=semaphore #### Semaphore-Konfiguration ```env -# Port-Binding (Standard: 3001) -SEMAPHORE_PORT=3001 +# Port-Binding (Standard: 9300) +SEMAPHORE_PORT=9300 # Admin-Benutzer SEMAPHORE_ADMIN=admin @@ -203,7 +205,7 @@ docker compose up -d ### 1. Projekt erstellen -1. Öffne http://localhost:3001 im Browser +1. Öffne http://localhost:9300 im Browser 2. Melde dich mit Admin-Credentials an 3. Klicke auf "New Project" 4. Gib einen Projektnamen ein (z.B. "My Project") @@ -289,9 +291,9 @@ Semaphore kann Docker-Images aus der lokalen Registry verwenden: ## Troubleshooting -### Port-Konflikt (Port 3000 vs 3001) +### Port-Konflikt (Port 3000 vs 9300) -**Problem**: Port 3000 ist standardmäßig von Gitea belegt, daher verwendet Semaphore Port 3001. +**Problem**: Port 3000 ist standardmäßig von Gitea belegt, daher verwendet Semaphore Port 9300. **Lösung**: Wenn du einen anderen Port verwenden möchtest, setze `SEMAPHORE_PORT` in der `.env` Datei: ```env @@ -309,7 +311,7 @@ docker compose logs semaphore **Häufige Ursachen**: - MySQL ist noch nicht bereit (warte auf Health-Check) -- Port 3001 ist bereits belegt: `netstat -tuln | grep 3001` (oder auf Windows: `netstat -ano | findstr :3001`) +- Port 9300 ist bereits belegt: `netstat -tuln | grep 9300` (oder auf Windows: `netstat -ano | findstr :9300`) - Falsche Datenbank-Credentials **Lösung**: @@ -382,26 +384,44 @@ docker compose exec semaphore wget --no-verbose --spider http://localhost:3000/a ## Sicherheit -### Lokale Entwicklung (Aktuell) +### 🔒 Lokaler Zugriff (Nur localhost) -- ✅ Nur localhost-Zugriff (127.0.0.1:3000) -- ✅ Isoliertes Netzwerk (kein externer Zugriff) -- ✅ Keine Traefik-Integration -- ⚠️ Standard-Passwörter (nur für lokale Entwicklung) +Semaphore ist absichtlich NUR lokal zugänglich aus Sicherheitsgründen: -### Für Produktion +- ✅ **Nur localhost-Zugriff**: Port 9300 gebunden an 127.0.0.1 (nicht 0.0.0.0) +- ✅ **Keine externen Netzwerke**: Kein externer Zugriff möglich +- ✅ **Keine Traefik-Integration**: Keine öffentliche Route konfiguriert +- ✅ **Isoliertes Netzwerk**: Nur internes Docker-Netzwerk +- ⚠️ **Standard-Passwörter**: Nur für lokale Entwicklung - ändern bei Bedarf -Wenn du Semaphore später für Produktion nutzen willst: +### ⚠️ KEINE Produktions-Nutzung über Internet! -1. **Starke Passwörter**: Ändere alle Passwörter in `.env` +**Semaphore sollte NICHT öffentlich zugänglich gemacht werden!** + +Gründe: +1. **Sicherheitsrisiko**: Semaphore hat Zugriff auf SSH-Keys und Deployment-Credentials +2. **Privilegierter Zugriff**: Kann auf Produktions-Server zugreifen +3. **Keine Multi-Factor-Authentication**: Standardmäßig keine 2FA +4. **Exploits**: Selbst-gehostete Software kann Sicherheitslücken haben + +### Für lokale Entwicklung (Empfohlen) + +Wenn du Semaphore lokal nutzen willst: + +1. **Starke Passwörter**: Ändere alle Passwörter in `.env`: + ```env + SEMAPHORE_ADMIN_PASSWORD= + MYSQL_PASSWORD= + MYSQL_ROOT_PASSWORD= + ``` 2. **Encryption Key**: Generiere einen sicheren Key: ```bash head -c32 /dev/urandom | base64 ``` -3. **Traefik-Integration**: Füge Traefik-Labels für HTTPS hinzu -4. **LDAP/SSO**: Konfiguriere externe Authentifizierung -5. **Backup-Strategie**: Regelmäßige MySQL-Backups einrichten -6. **Resource Limits**: Füge Memory/CPU-Limits hinzu + Setze in `.env`: `SEMAPHORE_ACCESS_KEY_ENCRYPTION=` +3. **SSH-Keys**: Stelle sicher, dass SSH-Keys sicher gespeichert sind +4. **Backup-Strategie**: Regelmäßige MySQL-Backups einrichten +5. **Resource Limits**: Füge Memory/CPU-Limits hinzu (optional) ## Wartung @@ -537,7 +557,7 @@ docker compose logs --tail=100 docker compose ps # Semaphore-Health -curl http://localhost:3001/api/health +curl http://localhost:9300/api/health # MySQL-Health docker compose exec mysql mysqladmin ping -h localhost -u root -psemaphore_root @@ -550,7 +570,7 @@ docker compose exec mysql mysqladmin ping -h localhost -u root -psemaphore_root **Nächste Schritte**: 1. `cp env.example .env` ausführen 2. `docker compose up -d` starten -3. http://localhost:3001 öffnen +3. http://localhost:9300 öffnen 4. Mit Admin-Credentials anmelden 5. Erstes Projekt und Template erstellen diff --git a/deployment/stacks/semaphore/SETUP_REPOSITORY.md b/deployment/stacks/semaphore/SETUP_REPOSITORY.md new file mode 100644 index 00000000..c5832c1f --- /dev/null +++ b/deployment/stacks/semaphore/SETUP_REPOSITORY.md @@ -0,0 +1,355 @@ +# Git-Repository in Semaphore Self-Hosted integrieren + +Diese Anleitung beschreibt, wie du dein Git-Repository in Semaphore Self-Hosted (Ansible-UI) integrierst. + +## ?? ?bersicht + +Semaphore Self-Hosted ist eine Web-UI f?r Ansible, die es erm?glicht: +- Ansible-Playbooks ?ber eine grafische Oberfl?che auszuf?hren +- CI/CD-Workflows mit Ansible zu automatisieren +- Git-Repositories als Playbook-Quellen zu verwenden + +## ?? Schritt 1: Semaphore starten + +### 1.1 Environment-Datei erstellen + +```bash +cd deployment/stacks/semaphore +cp env.example .env +``` + +### 1.2 Encryption Key generieren (WICHTIG!) + +```bash +# Linux/WSL +head -c32 /dev/urandom | base64 + +# Windows PowerShell +-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | % {[char]$_}) | ConvertTo-Base64 +``` + +Kopiere den generierten Key und setze ihn in `.env`: +```env +SEMAPHORE_ACCESS_KEY_ENCRYPTION= +``` + +### 1.3 Stack starten + +```bash +docker compose up -d +``` + +### 1.4 Web-UI ?ffnen + +?ffne im Browser: **http://localhost:9300** + +?? **WICHTIG**: Semaphore ist NUR lokal zug?nglich (127.0.0.1). Es gibt KEINEN externen Zugriff aus Sicherheitsgr?nden! + +**Standard-Login**: +- Username: `admin` +- Password: `admin` + +## ?? Schritt 2: Projekt in Semaphore erstellen + +### 2.1 Neues Projekt anlegen + +1. Melde dich in Semaphore an +2. Klicke auf **"New Project"** oder **"Create Project"** +3. Gib einen Projektnamen ein: **"michaelschiemer"** +4. Klicke auf **"Create"** + +### 2.2 Git-Repository als Playbook-Quelle hinzuf?gen + +Semaphore Self-Hosted kann Playbooks direkt aus Git-Repositories laden. Du hast zwei Optionen: + +**Option A: Playbooks aus Repository-Clone verwenden** (Empfohlen) + +1. Clone dein Repository lokal: +```bash +git clone https://git.michaelschiemer.de/michael/michaelschiemer.git +cd michaelschiemer +``` + +2. Erstelle ein Playbook-Verzeichnis f?r Semaphore: +```bash +mkdir -p deployment/stacks/semaphore/playbooks +cp deployment/ansible/playbooks/*.yml deployment/stacks/semaphore/playbooks/ +``` + +**Option B: Playbooks direkt in Semaphore einf?gen** + +Semaphore kann auch Playbooks direkt im Web-UI erstellen/bearbeiten. F?r Git-Integration ist Option A besser. + +## ??? Schritt 3: Inventory erstellen + +Ein Inventory definiert die Hosts, auf denen Playbooks ausgef?hrt werden. + +### 3.1 Inventory anlegen + +1. Gehe zu deinem Projekt ? **Inventories** ? **New Inventory** +2. Name: **"Production Hosts"** oder **"Local Hosts"** +3. Klicke auf **"Create"** + +### 3.2 Host hinzuf?gen + +1. Klicke auf dein Inventory ? **Add Host** +2. F?lle folgende Felder aus: + +**F?r Production-Deployment:** +``` +Name: production +Address: 94.16.110.151 +SSH Username: deploy +SSH Port: 22 +``` + +**F?r Staging-Deployment:** +``` +Name: staging +Address: 94.16.110.151 # oder dein Staging-Host +SSH Username: deploy +SSH Port: 22 +``` + +### 3.3 SSH-Key hinzuf?gen + +1. Gehe zu **Keys** (Seitenleiste) +2. Klicke auf **"New Key"** +3. Gib einen Namen ein: **"Deployment Key"** +4. F?ge deinen SSH Private Key ein (aus `~/.ssh/production` oder ?hnlich) +5. W?hle den Key im Host-Inventory aus + +## ?? Schritt 4: Template erstellen + +Templates verbinden Playbooks mit Inventories und definieren Parameter. + +### 4.1 Template f?r Tests erstellen + +1. Gehe zu **Templates** ? **New Template** +2. Template-Name: **"Run PHP Tests"** +3. Inventory: W?hle dein Inventory +4. Playbook: Erstelle ein Playbook oder verwende ein vorhandenes: + +```yaml +--- +- name: Run PHP Tests + hosts: localhost + gather_facts: no + tasks: + - name: Checkout repository + git: + repo: https://git.michaelschiemer.de/michael/michaelschiemer.git + dest: /tmp/ci-build + version: main + + - name: Install dependencies + command: composer install --no-interaction --prefer-dist + args: + chdir: /tmp/ci-build + + - name: Run tests + command: ./vendor/bin/pest + args: + chdir: /tmp/ci-build + register: test_result + + - name: Show test results + debug: + var: test_result.stdout_lines +``` + +5. Speichere das Template + +### 4.2 Template f?r Build erstellen + +Erstelle ein Template f?r Docker Image Build: + +```yaml +--- +- name: Build and Push Docker Image + hosts: localhost + gather_facts: no + tasks: + - name: Checkout repository + git: + repo: https://git.michaelschiemer.de/michael/michaelschiemer.git + dest: /tmp/ci-build + version: main + + - name: Login to Docker registry + docker_login: + username: "{{ registry_user }}" + password: "{{ registry_password }}" + registry_url: "{{ registry_url }}" + vars: + registry_user: "admin" + registry_url: "registry.michaelschiemer.de" + + - name: Build Docker image + docker_image: + name: "{{ registry_url }}/framework:{{ image_tag }}" + tag: "{{ image_tag }}" + source: build + build: + path: /tmp/ci-build + dockerfile: Dockerfile.production + push: yes + vars: + registry_url: "registry.michaelschiemer.de" + image_tag: "latest" +``` + +### 4.3 Template f?r Deployment erstellen + +Erstelle ein Template f?r Production-Deployment (verwendet die vorhandenen Ansible-Playbooks): + +```yaml +--- +- name: Deploy to Production + hosts: production + gather_facts: yes + become: yes + tasks: + - name: Checkout deployment scripts + git: + repo: https://git.michaelschiemer.de/michael/michaelschiemer.git + dest: /tmp/deployment + version: main + + - name: Include deployment playbook + include_role: + name: deployment/ansible/playbooks/deploy-update.yml +``` + +Oder verwende die vorhandenen Ansible-Playbooks direkt: + +**Vorteil**: Die vorhandenen Playbooks in `deployment/ansible/playbooks/` k?nnen direkt verwendet werden! + +1. Erstelle ein Template: **"Deploy to Production"** +2. W?hle Inventory: **"Production Hosts"** +3. Playbook-Pfad: **`deployment/ansible/playbooks/deploy-update.yml`** +4. Speichere + +## ?? Schritt 5: CI/CD-Workflow einrichten + +### 5.1 Git-Webhook konfigurieren (Optional) + +Semaphore kann Webhooks von Git-Repositories empfangen, um automatisch Tasks zu starten. + +1. Gehe zu deinem Git-Repository (Gitea) +2. Settings ? Webhooks ? Add Webhook +3. Webhook-URL: `http://localhost:9300/api/hook/git` (nur lokal!) +4. Content-Type: `application/json` +5. Secret: Optional, aber empfohlen + +### 5.2 Task manuell starten + +1. Gehe zu **Templates** +2. Klicke auf dein Template (z.B. "Run PHP Tests") +3. Klicke auf **"Run"** +4. Beobachte die Ausf?hrung in Echtzeit + +### 5.3 Task automatisch starten + +Semaphore kann Tasks basierend auf Git-Events starten: + +1. Gehe zu Template ? **Settings** +2. Aktiviere **"Auto Run on Push"** +3. W?hle Branch: `main` oder `staging` +4. Speichere + +## ?? Verwendung vorhandener Ansible-Playbooks + +### Vorhandene Playbooks verwenden + +Die vorhandenen Ansible-Playbooks in `deployment/ansible/playbooks/` k?nnen direkt in Semaphore verwendet werden: + +1. **Mounte Playbooks als Volume** (in `docker-compose.yml`): +```yaml +volumes: + - ./../../ansible/playbooks:/tmp/semaphore/playbooks:ro +``` + +2. **Erstelle Templates**, die auf diese Playbooks verweisen: + - Playbook-Pfad: `/tmp/semaphore/playbooks/deploy-update.yml` + - Inventory: W?hle dein Production-Inventory + +### Beispiel-Templates + +#### Template: Deploy Update +- Name: "Deploy Update" +- Inventory: "Production Hosts" +- Playbook: `/tmp/semaphore/playbooks/deploy-update.yml` +- Variables: + ```yaml + registry_url: registry.michaelschiemer.de + image_name: framework + image_tag: latest + ``` + +#### Template: Rollback +- Name: "Rollback" +- Inventory: "Production Hosts" +- Playbook: `/tmp/semaphore/playbooks/rollback.yml` +- Variables: + ```yaml + registry_url: registry.michaelschiemer.de + image_name: framework + ``` + +## ?? Erweiterte Konfiguration + +### Docker Socket f?r Build-Tasks + +F?r Docker-Build-Tasks muss der Semaphore-Container Zugriff auf den Docker-Socket haben: + +**In `docker-compose.yml` hinzuf?gen:** +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro +``` + +### Environment-Variablen f?r Templates + +Du kannst Environment-Variablen in Templates verwenden: + +1. Gehe zu Template ? **Variables** +2. F?ge Variablen hinzu: + - `registry_url`: `registry.michaelschiemer.de` + - `registry_user`: `admin` + - `image_name`: `framework` + +### Git-Integration mit SSH-Keys + +F?r private Repositories: + +1. Gehe zu **Keys** +2. Erstelle einen SSH-Key f?r Git-Access +3. F?ge den Public Key zu deinem Git-Repository hinzu (Deploy Keys) +4. Verwende SSH-URL in Git-Tasks: + ```yaml + git: + repo: git@git.michaelschiemer.de:michael/michaelschiemer.git + ``` + +## ?? N?tzliche Links + +- [Semaphore Self-Hosted Dokumentation](https://docs.ansible-semaphore.com/) +- [Ansible Semaphore GitHub](https://github.com/ansible-semaphore/semaphore) +- [Ansible Playbook Dokumentation](https://docs.ansible.com/ansible/latest/playbook_guide/index.html) + +## ? Checkliste + +- [ ] Semaphore gestartet (http://localhost:9300) +- [ ] Projekt erstellt +- [ ] Inventory mit Hosts erstellt +- [ ] SSH-Keys f?r Host-Zugriff konfiguriert +- [ ] Template f?r Tests erstellt +- [ ] Template f?r Build erstellt +- [ ] Template f?r Deployment erstellt +- [ ] Erstes Template erfolgreich ausgef?hrt +- [ ] Git-Webhook konfiguriert (optional) + +--- + +**N?chste Schritte**: Starte Semaphore und erstelle dein erstes Template! diff --git a/deployment/stacks/semaphore/docker-compose.yml b/deployment/stacks/semaphore/docker-compose.yml index c743fb01..311649f1 100644 --- a/deployment/stacks/semaphore/docker-compose.yml +++ b/deployment/stacks/semaphore/docker-compose.yml @@ -38,24 +38,12 @@ services: networks: - semaphore-internal ports: - # Only bind to localhost, not external interfaces - # Default port 3001 to avoid conflict with Gitea (port 3000) - - "127.0.0.1:${SEMAPHORE_PORT:-3001}:3000" - labels: - # Traefik configuration - - "traefik.enable=true" - # HTTP Router (redirects to HTTPS) - - "traefik.http.routers.semaphore.rule=Host(`semaphore.michaelschiemer.de`)" - - "traefik.http.routers.semaphore.entrypoints=web" - - "traefik.http.routers.semaphore.middlewares=redirect-to-https" - # HTTPS Router - - "traefik.http.routers.semaphore-secure.rule=Host(`semaphore.michaelschiemer.de`)" - - "traefik.http.routers.semaphore-secure.entrypoints=websecure" - - "traefik.http.routers.semaphore-secure.tls=true" - - "traefik.http.routers.semaphore-secure.tls.certresolver=letsencrypt" - - "traefik.http.routers.semaphore-secure.service=semaphore" - # Service definition (use localhost port binding) - # Note: Dynamic config in traefik/dynamic/semaphore.yml takes precedence + # ONLY bind to localhost (127.0.0.1) - NOT accessible externally! + # Default port 9300 to avoid conflict with Gitea (port 3000) + # SECURITY: This ensures Semaphore is only accessible locally + - "127.0.0.1:${SEMAPHORE_PORT:-9300}:3000" + # NO Traefik labels - Semaphore should only be accessible locally! + # External access is disabled for security reasons. environment: - TZ=Europe/Berlin # Database Configuration @@ -80,6 +68,9 @@ services: - SEMAPHORE_WEBHOOK_URL=${SEMAPHORE_WEBHOOK_URL:-} volumes: - semaphore-data:/etc/semaphore + # Mount playbooks from repository so Semaphore can access them + - ../../../deployment/stacks/semaphore/playbooks:/tmp/semaphore/playbooks:ro + - ../../../deployment/ansible/playbooks:/tmp/semaphore/repo-playbooks:ro - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro healthcheck: diff --git a/deployment/stacks/semaphore/env.example b/deployment/stacks/semaphore/env.example index 1af7891d..4a069b33 100644 --- a/deployment/stacks/semaphore/env.example +++ b/deployment/stacks/semaphore/env.example @@ -14,10 +14,10 @@ MYSQL_PASSWORD=semaphore # Semaphore Configuration # ============================================ -# Port binding (default: 3001) +# Port binding (default: 9300) # Only accessible via localhost (127.0.0.1) # Note: Changed from 3000 to avoid conflict with Gitea -SEMAPHORE_PORT=3001 +SEMAPHORE_PORT=9300 # Admin User Configuration SEMAPHORE_ADMIN=admin diff --git a/deployment/stacks/semaphore/playbooks/ci-tests.yml b/deployment/stacks/semaphore/playbooks/ci-tests.yml new file mode 100644 index 00000000..be7b997a --- /dev/null +++ b/deployment/stacks/semaphore/playbooks/ci-tests.yml @@ -0,0 +1,103 @@ +--- +# CI Tests Playbook f?r Semaphore +# F?hrt PHP Tests und Quality Checks aus + +- name: Run CI Tests and Quality Checks + hosts: localhost + gather_facts: no + vars: + repo_url: "{{ repo_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}" + repo_branch: "{{ repo_branch | default('main') }}" + build_dir: "/tmp/ci-build" + + tasks: + - name: Clean up build directory + file: + path: "{{ build_dir }}" + state: absent + + - name: Checkout repository + git: + repo: "{{ repo_url }}" + dest: "{{ build_dir }}" + version: "{{ repo_branch }}" + force: yes + register: git_result + + - name: Display checked out commit + debug: + msg: "Checked out commit: {{ git_result.after }}" + + - name: Install Composer if not present + get_url: + url: https://getcomposer.org/installer + dest: /tmp/composer-installer.php + mode: '0755' + when: ansible_facts.os_family == "Debian" + + - name: Install Composer (Debian) + shell: php /tmp/composer-installer.php && mv composer.phar /usr/local/bin/composer + when: ansible_facts.os_family == "Debian" + args: + creates: /usr/local/bin/composer + + - name: Install PHP dependencies + command: composer install --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=php + args: + chdir: "{{ build_dir }}" + register: composer_result + + - name: Display composer output + debug: + var: composer_result.stdout_lines + when: composer_result.stdout_lines is defined + + - name: Run PHP tests + command: ./vendor/bin/pest + args: + chdir: "{{ build_dir }}" + register: test_result + ignore_errors: yes + + - name: Display test results + debug: + msg: "{{ test_result.stdout_lines }}" + when: test_result.stdout_lines is defined + + - name: Run PHPStan + command: composer phpstan + args: + chdir: "{{ build_dir }}" + register: phpstan_result + ignore_errors: yes + when: test_result.rc == 0 + + - name: Display PHPStan results + debug: + msg: "{{ phpstan_result.stdout_lines }}" + when: phpstan_result.stdout_lines is defined and phpstan_result.rc == 0 + + - name: Run code style check + command: composer cs + args: + chdir: "{{ build_dir }}" + register: cs_result + ignore_errors: yes + when: test_result.rc == 0 + + - name: Display code style results + debug: + msg: "{{ cs_result.stdout_lines }}" + when: cs_result.stdout_lines is defined + + - name: Fail if tests failed + fail: + msg: "Tests failed! Check output above." + when: test_result.rc != 0 + + - name: Summary + debug: + msg: + - "? CI Tests completed successfully!" + - "Commit: {{ git_result.after }}" + - "Branch: {{ repo_branch }}" diff --git a/deployment/stacks/semaphore/playbooks/deploy-production.yml b/deployment/stacks/semaphore/playbooks/deploy-production.yml new file mode 100644 index 00000000..6f0f5d8f --- /dev/null +++ b/deployment/stacks/semaphore/playbooks/deploy-production.yml @@ -0,0 +1,129 @@ +--- +# Production Deployment Playbook f?r Semaphore +# Deployed die Anwendung auf Production-Server + +- name: Deploy to Production + hosts: production + gather_facts: yes + become: yes + vars: + registry_url: "{{ registry_url | default('registry.michaelschiemer.de') }}" + image_name: "{{ image_name | default('framework') }}" + image_tag: "{{ image_tag | default('latest') }}" + registry_user: "{{ registry_user | default('admin') }}" + registry_password: "{{ registry_password | required }}" + deployment_path: "{{ deployment_path | default('~/deployment/stacks/application') }}" + repo_url: "{{ repo_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}" + + tasks: + - name: Display deployment info + debug: + msg: + - "?? Starting production deployment..." + - "Host: {{ inventory_hostname }}" + - "Registry: {{ registry_url }}" + - "Image: {{ image_name }}:{{ image_tag }}" + - "Path: {{ deployment_path }}" + + - name: Ensure deployment directory exists + file: + path: "{{ deployment_path }}" + state: directory + mode: '0755' + + - name: Login to Docker registry + docker_login: + username: "{{ registry_user }}" + password: "{{ registry_password }}" + registry_url: "{{ registry_url }}" + + - name: Pull Docker image + docker_image: + name: "{{ registry_url }}/{{ image_name }}:{{ image_tag }}" + source: pull + + - name: Check if docker-compose files exist + stat: + path: "{{ deployment_path }}/docker-compose.base.yml" + register: base_compose_exists + + - name: Check if docker-compose.production.yml exists + stat: + path: "{{ deployment_path }}/docker-compose.production.yml" + register: production_compose_exists + + - name: Copy docker-compose files if missing + copy: + src: "{{ item.src }}" + dest: "{{ deployment_path }}/{{ item.dest }}" + mode: '0644' + loop: + - { src: "docker-compose.base.yml", dest: "docker-compose.base.yml" } + - { src: "docker-compose.production.yml", dest: "docker-compose.production.yml" } + when: not (item.dest == "docker-compose.base.yml" and base_compose_exists.stat.exists) or + not (item.dest == "docker-compose.production.yml" and production_compose_exists.stat.exists) + delegate_to: localhost + become: no + + - name: Update docker-compose.production.yml with new image + replace: + path: "{{ deployment_path }}/docker-compose.production.yml" + regexp: 'image:.*{{ image_name }}:.*' + replace: 'image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}' + + - name: Update docker-compose.production.yml with new image (alternative format) + replace: + path: "{{ deployment_path }}/docker-compose.production.yml" + regexp: 'image:.*{{ image_name }}@.*' + replace: 'image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}' + + - name: Ensure Docker networks exist + docker_network: + name: traefik-public + state: present + ignore_errors: yes + + - name: Deploy services + docker_compose_v2: + project_src: "{{ deployment_path }}" + files: + - "{{ deployment_path }}/docker-compose.base.yml" + - "{{ deployment_path }}/docker-compose.production.yml" + pull: missing + state: present + recreate: always + + - name: Wait for services to start + pause: + seconds: 15 + + - name: Check container status + command: docker compose -f {{ deployment_path }}/docker-compose.base.yml -f {{ deployment_path }}/docker-compose.production.yml ps + register: container_status + + - name: Display container status + debug: + var: container_status.stdout_lines + + - name: Health check + uri: + url: https://michaelschiemer.de/health + method: GET + status_code: [200, 201, 204] + validate_certs: no + register: health_result + retries: 10 + delay: 10 + until: health_result.status == 200 + ignore_errors: yes + + - name: Display health check result + debug: + msg: "? Health check passed!" if health_result.status == 200 else "?? Health check failed" + + - name: Summary + debug: + msg: + - "? Production deployment completed!" + - "Image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}" + - "URL: https://michaelschiemer.de" diff --git a/deployment/stacks/semaphore/playbooks/deploy-staging.yml b/deployment/stacks/semaphore/playbooks/deploy-staging.yml new file mode 100644 index 00000000..831b2b56 --- /dev/null +++ b/deployment/stacks/semaphore/playbooks/deploy-staging.yml @@ -0,0 +1,110 @@ +--- +# Staging Deployment Playbook f?r Semaphore +# Deployed die Anwendung auf Staging-Server + +- name: Deploy to Staging + hosts: staging + gather_facts: yes + become: yes + vars: + registry_url: "{{ registry_url | default('registry.michaelschiemer.de') }}" + image_name: "{{ image_name | default('framework') }}" + image_tag: "{{ image_tag | default('latest') }}" + registry_user: "{{ registry_user | default('admin') }}" + registry_password: "{{ registry_password | required }}" + deployment_path: "{{ deployment_path | default('~/deployment/stacks/staging') }}" + repo_url: "{{ repo_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}" + + tasks: + - name: Display deployment info + debug: + msg: + - "?? Starting staging deployment..." + - "Host: {{ inventory_hostname }}" + - "Registry: {{ registry_url }}" + - "Image: {{ image_name }}:{{ image_tag }}" + - "Path: {{ deployment_path }}" + + - name: Ensure deployment directory exists + file: + path: "{{ deployment_path }}" + state: directory + mode: '0755' + + - name: Login to Docker registry + docker_login: + username: "{{ registry_user }}" + password: "{{ registry_password }}" + registry_url: "{{ registry_url }}" + + - name: Pull Docker image + docker_image: + name: "{{ registry_url }}/{{ image_name }}:{{ image_tag }}" + source: pull + + - name: Check if docker-compose files exist + stat: + path: "{{ deployment_path }}/docker-compose.base.yml" + register: base_compose_exists + + - name: Check if docker-compose.staging.yml exists + stat: + path: "{{ deployment_path }}/docker-compose.staging.yml" + register: staging_compose_exists + + - name: Copy docker-compose files if missing + copy: + src: "{{ item.src }}" + dest: "{{ deployment_path }}/{{ item.dest }}" + mode: '0644' + loop: + - { src: "docker-compose.base.yml", dest: "docker-compose.base.yml" } + - { src: "docker-compose.staging.yml", dest: "docker-compose.staging.yml" } + when: not (item.dest == "docker-compose.base.yml" and base_compose_exists.stat.exists) or + not (item.dest == "docker-compose.staging.yml" and staging_compose_exists.stat.exists) + delegate_to: localhost + become: no + + - name: Update docker-compose.staging.yml with new image + replace: + path: "{{ deployment_path }}/docker-compose.staging.yml" + regexp: 'image:.*{{ image_name }}:.*' + replace: 'image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}' + + - name: Ensure Docker networks exist + docker_network: + name: "{{ item }}" + state: present + loop: + - traefik-public + - staging-internal + ignore_errors: yes + + - name: Deploy services + docker_compose_v2: + project_src: "{{ deployment_path }}" + files: + - "{{ deployment_path }}/docker-compose.base.yml" + - "{{ deployment_path }}/docker-compose.staging.yml" + pull: missing + state: present + recreate: always + + - name: Wait for services to start + pause: + seconds: 15 + + - name: Check container status + command: docker compose -f {{ deployment_path }}/docker-compose.base.yml -f {{ deployment_path }}/docker-compose.staging.yml ps + register: container_status + + - name: Display container status + debug: + var: container_status.stdout_lines + + - name: Summary + debug: + msg: + - "? Staging deployment completed!" + - "Image: {{ registry_url }}/{{ image_name }}:{{ image_tag }}" + - "URL: https://staging.michaelschiemer.de" diff --git a/deployment/stacks/semaphore/playbooks/docker-build.yml b/deployment/stacks/semaphore/playbooks/docker-build.yml new file mode 100644 index 00000000..5b04c304 --- /dev/null +++ b/deployment/stacks/semaphore/playbooks/docker-build.yml @@ -0,0 +1,103 @@ +--- +# Docker Build Playbook f?r Semaphore +# Baut Docker Image und pusht es zur Registry + +- name: Build and Push Docker Image + hosts: localhost + gather_facts: no + vars: + repo_url: "{{ repo_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}" + repo_branch: "{{ repo_branch | default('main') }}" + build_dir: "/tmp/ci-build" + registry_url: "{{ registry_url | default('registry.michaelschiemer.de') }}" + image_name: "{{ image_name | default('framework') }}" + image_tag: "{{ image_tag | default('latest') }}" + registry_user: "{{ registry_user | default('admin') }}" + registry_password: "{{ registry_password | required }}" + + tasks: + - name: Clean up build directory + file: + path: "{{ build_dir }}" + state: absent + + - name: Checkout repository + git: + repo: "{{ repo_url }}" + dest: "{{ build_dir }}" + version: "{{ repo_branch }}" + force: yes + register: git_result + + - name: Get short commit SHA + shell: echo "{{ git_result.after }}" | cut -c1-7 + register: short_sha + + - name: Generate image tags + set_fact: + tags: + - "{{ registry_url }}/{{ image_name }}:latest" + - "{{ registry_url }}/{{ image_name }}:{{ image_tag }}" + - "{{ registry_url }}/{{ image_name }}:git-{{ short_sha.stdout }}" + + - name: Display image tags + debug: + msg: + - "??? Building Docker image..." + - "Registry: {{ registry_url }}" + - "Image: {{ image_name }}" + - "Tags: {{ tags | join(', ') }}" + + - name: Ensure Docker is available + command: docker --version + register: docker_version + + - name: Display Docker version + debug: + msg: "Docker version: {{ docker_version.stdout }}" + + - name: Login to Docker registry + docker_login: + username: "{{ registry_user }}" + password: "{{ registry_password }}" + registry_url: "{{ registry_url }}" + register: login_result + + - name: Verify registry login + debug: + msg: "? Successfully logged in to {{ registry_url }}" + when: login_result.failed == false + + - name: Build Docker image + docker_image: + name: "{{ registry_url }}/{{ image_name }}" + tag: "{{ image_tag }}" + source: build + build: + path: "{{ build_dir }}" + dockerfile: Dockerfile.production + push: yes + state: present + register: build_result + + - name: Tag image with additional tags + docker_image: + name: "{{ registry_url }}/{{ image_name }}:{{ image_tag }}" + repository: "{{ registry_url }}/{{ image_name }}" + tag: "{{ item }}" + source: local + push: yes + state: present + loop: + - "latest" + - "git-{{ short_sha.stdout }}" + when: build_result.changed + + - name: Summary + debug: + msg: + - "? Docker image built and pushed successfully!" + - "Registry: {{ registry_url }}" + - "Image: {{ image_name }}" + - "Tags: {{ tags | join(', ') }}" + - "Commit: {{ git_result.after }}" diff --git a/deployment/stacks/traefik/README.md b/deployment/stacks/traefik/README.md index 5f11572b..28fae4cb 100644 --- a/deployment/stacks/traefik/README.md +++ b/deployment/stacks/traefik/README.md @@ -16,6 +16,63 @@ Traefik acts as the central reverse proxy for all services, handling: - Zus?tzlich durch BasicAuth gesch?tzt - ?ffentlicher Zugriff ist blockiert +## Local Development + +For local development, use the separate local configuration to avoid port conflicts and Let's Encrypt errors: + +### Quick Start (Local Development) + +```bash +# Ensure Docker network exists +docker network create traefik-public 2>/dev/null || true + +# Start Traefik with local configuration +cd deployment/stacks/traefik +docker compose -f docker-compose.local.yml up -d + +# Check logs +docker compose -f docker-compose.local.yml logs -f + +# Access dashboard at http://localhost:8080/dashboard/ +``` + +### Local Development Configuration + +The local configuration (`docker-compose.local.yml` and `traefik.local.yml`) differs from production: + +- **Bridge network** instead of `host` mode (avoids port conflicts) +- **Port mappings**: `8080:80` only (HTTP-only for local development) + - Note: HTTPS not needed locally - avoids port conflicts with web container (8443:443) +- **HTTP-only** (no ACME/Let's Encrypt) for local development +- **Dashboard**: Accessible at `http://localhost:8080/dashboard/` (HTTP, no authentication) + - Also available: `http://localhost:8080/api/rawdata` and `http://localhost:8080/api/http/routers` +- **No `acme.json`** required +- **Console logging** (human-readable) instead of JSON file logs + +### Local Development vs Production + +| Feature | Local (`docker-compose.local.yml`) | Production (`docker-compose.yml`) | +|---------|-----------------------------------|----------------------------------| +| Network Mode | Bridge | Host | +| Ports | 8080:80 (HTTP only) | Direct binding (80, 443) | +| SSL/TLS | HTTP-only | HTTPS with Let's Encrypt | +| Dashboard | `http://localhost:8080/dashboard/` | `https://traefik.michaelschiemer.de` | +| Authentication | None (local dev) | VPN + BasicAuth | +| Logging | Console (human-readable) | JSON files | +| ACME | Disabled | Enabled | + +### Troubleshooting Local Development + +**Container restarts in loop:** +- Check if port 8080 is already in use: `netstat -tlnp | grep ':8080' || ss -tlnp | grep ':8080'` +- Verify Docker network exists: `docker network ls | grep traefik-public` +- Check logs: `docker compose -f docker-compose.local.yml logs -f traefik` + +**Services not accessible through Traefik:** +- Ensure services are on `traefik-public` network +- Verify Traefik labels are correctly configured +- Check that services are running: `docker compose ps` + ## Prerequisites 1. **Docker Network** diff --git a/deployment/stacks/traefik/docker-compose.local.yml b/deployment/stacks/traefik/docker-compose.local.yml new file mode 100644 index 00000000..65419966 --- /dev/null +++ b/deployment/stacks/traefik/docker-compose.local.yml @@ -0,0 +1,59 @@ +# Local Development Configuration for Traefik +# Usage: docker compose -f docker-compose.local.yml up -d +# +# This configuration is optimized for local development: +# - Bridge network instead of host mode +# - Port mapping: 8080:80 (HTTP only - HTTPS not needed for local dev) +# Note: 8443:443 is used by the web container, and we don't need HTTPS for Traefik locally +# - No ACME/Let's Encrypt (HTTP-only) +# - Simplified healthcheck + +services: + traefik: + image: traefik:v3.0 + container_name: traefik-local + restart: unless-stopped + security_opt: + - no-new-privileges:true + # Use bridge network for local development (avoids port conflicts) + # Ports 80/443 might be in use by other services + # For local development, we only use HTTP (no HTTPS needed) + # Note: 8443:443 is used by the web container + ports: + - "8080:80" # HTTP on port 80 (mapped to host port 8080) + - "8080:8080" # Traefik API entrypoint (for api.insecure=true dashboard) + environment: + - TZ=Europe/Berlin + volumes: + # Docker socket for service discovery + - /var/run/docker.sock:/var/run/docker.sock:ro + # Static configuration for local development + - ./traefik.local.yml:/traefik.yml:ro + # Dynamic configuration (shared with production config) + # Note: These configs reference letsencrypt resolver which we don't configure locally + # This will cause harmless errors in logs but won't break functionality + - ./dynamic:/dynamic:ro + networks: + - traefik-public + labels: + # Note: With api.insecure=true, Traefik should automatically serve the dashboard + # at /dashboard/ and /api/ without needing router labels. + # However, if this doesn't work in bridge network mode, we may need explicit routing. + # For now, we'll try without labels and see if api.insecure=true works directly. + - "traefik.enable=true" + healthcheck: + # Use wget or curl to check Traefik ping endpoint + # The ping endpoint is configured in traefik.local.yml on the 'web' entrypoint + # Try ping endpoint first, if that fails, try API endpoint + test: ["CMD-SHELL", "wget --quiet --spider http://localhost:80/ping || wget --quiet --spider http://localhost:80/api/rawdata || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + +networks: + traefik-public: + external: true + # Create this network if it doesn't exist: + # docker network create traefik-public + diff --git a/deployment/stacks/traefik/dynamic/semaphore.yml b/deployment/stacks/traefik/dynamic/semaphore.yml.disabled similarity index 100% rename from deployment/stacks/traefik/dynamic/semaphore.yml rename to deployment/stacks/traefik/dynamic/semaphore.yml.disabled diff --git a/deployment/stacks/traefik/traefik.local.yml b/deployment/stacks/traefik/traefik.local.yml new file mode 100644 index 00000000..3666308c --- /dev/null +++ b/deployment/stacks/traefik/traefik.local.yml @@ -0,0 +1,98 @@ +# Local Development Configuration for Traefik +# This configuration is optimized for local development without Let's Encrypt/ACME + +# Global Configuration +global: + checkNewVersion: true + sendAnonymousUsage: false + +# API and Dashboard +# For local development, we enable insecure access on port 8080 +# Dashboard automatically accessible at: +# - http://localhost:8080/dashboard/ (with trailing slash) +# - http://localhost:8080/api/rawdata +# - http://localhost:8080/api/http/routers +api: + dashboard: true + insecure: true # HTTP-only for local development - enables direct dashboard access + # Note: With insecure=true, dashboard is accessible at: + # - http://localhost:8080/dashboard/ + # - http://localhost:8080/api/rawdata + # - http://localhost:8080/api/http/routers + # The insecure mode works directly on the entrypoint (web) without needing router labels + +# Entry Points +entryPoints: + web: + address: ":80" + # No redirects for local development - HTTP is acceptable + + websecure: + address: ":443" + # Note: Even though we don't use HTTPS locally, we need this entrypoint + # because dynamic configurations (gitea.yml, semaphore.yml) reference it + # We use HTTP only, but the entrypoint must exist to avoid errors + + traefik: + address: ":8080" + # This entrypoint is used by api.insecure=true for dashboard access + # It must be on port 8080 (which maps to host port 8080) to match our port mapping + +# Certificate Resolvers +# Note: For local development, we don't configure ACME/Let's Encrypt +# Dynamic configs (gitea.yml, semaphore.yml) that reference letsencrypt will show errors +# but won't break Traefik functionality. We can ignore these errors for local dev. +# If you need to test with real certificates locally, configure ACME manually. +# certificatesResolvers: +# letsencrypt: +# acme: +# email: your-email@example.com +# storage: /tmp/acme.json +# caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" + +# Providers +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + # Use Docker bridge network for local development + network: traefik-public + watch: true + + file: + directory: /dynamic + watch: true + # Note: Dynamic configs (gitea.yml, semaphore.yml) will show errors + # because they reference letsencrypt resolver which we don't configure locally + # These errors are harmless and won't affect local development + +# Forwarded Headers Configuration +# Simplified for local development +forwardedHeaders: + trustedIPs: + - "127.0.0.1/32" # Localhost + - "172.17.0.0/16" # Docker bridge network + - "172.18.0.0/16" # Docker user-defined networks + insecure: true # Allow insecure forwarded headers for local dev + +# Logging - Console output for local development (easier to debug) +log: + level: INFO + format: common # Human-readable format for local development + +# Access Logs - Console output for local development +accessLog: + format: common # Human-readable format for local development + +# Metrics (optional for local development) +# Can be enabled if needed for monitoring +# metrics: +# prometheus: +# addEntryPointsLabels: true +# addRoutersLabels: true +# addServicesLabels: true + +# Ping endpoint for health checks +ping: + entryPoint: web + diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 500419aa..caa76b9d 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -12,7 +12,7 @@ services: container_name: web ports: - "8888:80" - - "8443:443" + - "443:443" # HTTPS auf Standard-Port 443 für direkten Zugriff via https://localhost environment: - APP_ENV=${APP_ENV:-development} volumes: