docs: vollständige Dokumentation (Dokploy-Deployment, Auto-Deploy, Troubleshooting, Architektur)

This commit is contained in:
Hermes
2026-08-12 20:20:27 +00:00
parent 32c29b1594
commit 5c481d7031
5 changed files with 540 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# ⚡ TECHNO-TODO
90s-Terminal/Cyberpunk-Style Todo-App. Bewusst als **Proof-of-Concept** gebaut, um den
kompletten Selbsthosting-Workflow zu testen:
**Gitea (Git) → Dokploy (PaaS) → Docker Swarm → Traefik → Live im LAN**
## Tech-Stack
- **Runtime:** Node.js 22 (Alpine)
- **Framework:** Express 4
- **Datenbank:** SQLite via `better-sqlite3` (bewusst statt PostgreSQL — siehe unten)
- **Templating:** EJS
- **Style:** 90s CRT/Neon-Look (grüne Scanlines, pinker Glow, monospace)
## Warum SQLite statt PostgreSQL?
Die erste Version nutzte PostgreSQL über Dokploy's Datenbank-Provisioning. Dabei traten
zwei Probleme auf:
1. **DB-Service wurde nie gestartet** — der `techno-db` Docker-Service existierte nicht,
die App crashte mit `ECONNREFUSED` / `ENOTFOUND`.
2. Der Dokploy-Deploy-Worker hatte zusätzlich einen **abgelaufenen API-Key** und die
Services waren **out-of-sync** (manuell umbenannt).
Für den POC wurde daher auf SQLite umgestellt — **keine externe DB nötig**, die App ist
in sich abgeschlossen. Für eine Produktiv-App mit echter Datenbank siehe
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
## Schnellstart (lokal)
```bash
npm install
npm start
# → http://localhost:3000
```
## Schnellstart (Docker)
```bash
docker build -t techno-todo .
docker run -p 3000:3000 techno-todo
```
## Dokumentation
| Datei | Inhalt |
|-------|--------|
| [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) | **Komplette Dokploy-Deployment-Anleitung** — nativer Weg (UI/API) + was tatsächlich gemacht wurde |
| [`docs/AUTODEPLOY.md`](docs/AUTODEPLOY.md) | **Auto-Deploy-Pipeline** — Gitea-Push → automatischer Build + Deploy |
| [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) | Alle Fehler, die beim POC aufgetreten sind, und ihre Lösungen |
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Netzwerk-Topologie und Infrastruktur-Übersicht |
## Live-URL
```
http://techno-todo.192.168.0.184.sslip.io/
```
(Erreichbar aus dem Heimnetz `192.168.0.x`. Die öffentliche IP `152.53.197.186` routet
aktuell nicht zu Dokploy — siehe `docs/ARCHITECTURE.md`.)
+88
View File
@@ -0,0 +1,88 @@
# Architektur & Infrastruktur
Übersicht der beteiligten Systeme und Netzwerk-Topologie.
## Komponenten
| System | Rolle | Adresse |
|--------|-------|---------|
| **Hermes-Server** | Assistent + Webhook-Receiver + Download-Hub | `192.168.0.80` |
| **Dokploy-Host** | PaaS (Docker Swarm) | `192.168.0.184` |
| **Gitea** | Git-Server | `git.kgessner.de` (K8s/Talos) |
| **Proxmox** | Hypervisor (Dokploy-VM + K8s-Cluster) | — |
| **Netcup-Gateway** | Reverse-Proxy (öffentliche IP) | `192.168.0.222` |
## Netzwerk-Topologie
```
Internet
│
152.53.197.186 (öffentliche IP)
│
┌────────┴────────┐
│ Netcup-Gateway │ (192.168.0.222)
│ Reverse-Proxy │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
git.kgessner.de dokploy.kgessner.de (weitere Dienste)
│ │
▼ ▼
K8s/Talos-Cluster Docker-Swarm-Host
(auf Proxmox) (192.168.0.184)
- Gitea - Dokploy UI (:3000)
- Traefik (:80/443)
- App-Services
```
### Wichtige Erkenntnisse
1. **`git.kgessner.de` und `dokploy.kgessner.de` teilen sich die öffentliche IP**
`152.53.197.186`. Ein Reverse-Proxy routet anhand des Host-Headers.
2. **Gitea läuft in Kubernetes (Talos)**, Dokploy in **Docker Swarm** — zwei
unterschiedliche Orchestrierungswelten auf demselben Proxmox-Host.
3. **Netzwerk-Isolation:** Der K8s-Cluster (Gitea) kann interne `192.168.0.x`-Adressen
(Hermes, Dokploy) nicht zuverlässig erreichen. Das ist der Grund, warum der
Gitea-Webhook unzuverlässig ist und ein Cron-Polling-Fallback nötig wurde.
4. **Öffentliche IP routet nicht zu Dokploy:** `techno-todo.152.53.197.186.sslip.io`
funktioniert **nicht**, weil Port 80/443 der öffentlichen IP nicht auf den
Dokploy-Host (`192.168.0.184`) weitergeleitet wird. Für LAN-Tests wird deshalb
`techno-todo.192.168.0.184.sslip.io` genutzt.
## Docker-Swarm auf Dokploy
```
docker service ls
# dokploy (PaaS-UI)
# dokploy-postgres (interne DB)
# dokploy-traefik (Reverse-Proxy, Ports 80/443)
# techno-todo-v2-... (diese App)
```
### Traefik-Konfiguration
Dokploy schreibt die Routing-Regeln nach `/etc/dokploy/traefik/dynamic/<app>.yml`
(Bind-Mount in den Traefik-Container).
```bash
# Reload nach Config-Änderung
docker kill -s HUP dokploy-traefik
```
### Dokploy-interne DB
PostgreSQL im Container `dokploy-postgres`. Nützliche Tabellen: `application`, `gitea`,
`apikey`, `project`, `domain`, `deployment`.
```bash
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy -c "\dt"
```
## sslip.io — der Magic-DNS-Trick
`<name>.192.168.0.184.sslip.io` löst automatisch zu `192.168.0.184` auf. So braucht jede
App keine eigene DNS-Config — einfach Subdomain vor die IP setzen.
+127
View File
@@ -0,0 +1,127 @@
# Auto-Deploy-Pipeline
Ziel: **`git push` = neue Version automatisch live**, ohne manuellen Eingriff.
## Überblick
```
Code ändern → git push (Gitea) → ┬─ Webhook → Hermes-Receiver → SSH → Dokploy → Build + Deploy
└─ Cron-Polling (alle 2 Min) → Dokploy → Build + Deploy
```
Es gibt **zwei unabhängige Trigger**, weil der Gitea-Webhook unzuverlässig ist (siehe
[„Warum zwei Trigger?"](#warum-zwei-trigger)):
| Mechanismus | Latenz | Zuverlässigkeit |
|-------------|--------|-----------------|
| Gitea-Webhook → Receiver | ~5 s | ⚠️ sporadisch |
| Cron-Polling | ≤ 2 min | ✅ garantiert |
---
## Komponenten
### 1. Gitea-Webhook
Per Gitea-API erstellt:
```bash
curl -X POST "https://git.kgessner.de/api/v1/repos/LuiiCode/techno-todo/hooks" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "gitea",
"config": {"url": "http://192.168.0.80:8002/hook", "content_type": "json"},
"events": ["push"],
"active": true
}'
```
Zeigt auf den Webhook-Receiver auf dem Hermes-Server (`192.168.0.80:8002`).
### 2. Webhook-Receiver (Hermes)
Datei: `/home/hermes/webhook-receiver.py`
- Python-Stdlib-`http.server` (keine Dependencies)
- Lauscht auf `0.0.0.0:8002`
- Bei `X-Gitea-Event: push` auf `refs/heads/main` des Ziel-Repos:
- startet **asynchron** (Thread) `ssh root@192.168.0.184 /opt/deploy-techno-todo.sh`
- antwortet sofort mit `200` (Gitea wartet nicht auf den Deploy)
- Loggt in `/home/hermes/webhook-receiver.log`
### 3. Deploy-Skript (Dokploy-Host)
Datei: `/opt/deploy-techno-todo.sh`
**Idempotent** — deployt nur, wenn es neue Commits gibt:
```bash
#!/bin/bash
set -euo pipefail
REPO_DIR="/opt/repos/techno-todo"
SERVICE_NAME="techno-todo-v2-mzs9mj"
cd "$REPO_DIR"
git fetch --all --prune >/dev/null 2>&1
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
[ "$LOCAL" = "$REMOTE" ] && { echo "UP-TO-DATE: $LOCAL"; exit 0; }
git reset --hard origin/main
COMMIT=$(git rev-parse --short HEAD)
docker build -t "techno-todo:${COMMIT}" .
docker tag "techno-todo:${COMMIT}" techno-todo:latest
docker service update --force --image "techno-todo:${COMMIT}" "$SERVICE_NAME"
echo "DONE: deployed ${COMMIT}"
```
Das Repo liegt persistent unter `/opt/repos/techno-todo` auf dem Dokploy-Host.
### 4. Cron-Polling (Fallback)
Auf dem Dokploy-Host:
```bash
*/2 * * * * /opt/deploy-techno-todo.sh >> /var/log/techno-todo-deploy.log 2>&1
```
Da das Skript idempotent ist, passiert bei „keine neuen Commits" nichts (exit 0).
---
## Warum zwei Trigger?
Der Gitea-Webhook erwies sich beim POC als **unzuverlässig**: Von mehreren Pushes wurden
nur einige ausgelöst, andere gingen verloren.
**Root-Cause:** Gitea läuft in einem **Kubernetes-Cluster (Talos)** auf Proxmox — also in
einem anderen Netzwerk-Kontext als der Dokploy-Docker-Swarm-Host (`192.168.0.184`) und der
Hermes-Server (`192.168.0.80`). Ausgehende Verbindungen aus dem K8s-Cluster zu internen
`192.168.0.x`-Adressen sind nicht zuverlässig möglich (CNI/NAT-Isolation).
Der Cron-Polling-Fallback umgeht das komplett: Er läuft direkt **auf** dem Dokploy-Host und
holt sich den Git-Stand aktiv ab, statt auf einen eingehenden Webhook zu warten.
---
## Setup-Reihenfolge (für eine neue App)
1. Repo persistent auf Dokploy klonen: `/opt/repos/<app>`
2. Deploy-Skript anpassen (Repo-Dir + Service-Name) → `/opt/deploy-<app>.sh`
3. Cron-Job auf Dokploy einrichten (alle 2 Min)
4. (Optional) Webhook-Receiver auf Hermes starten + Gitea-Webhook einrichten
## Verifikation
```bash
# 1. Push machen
git push
# 2. Nach ~2 Min prüfen:
ssh root@192.168.0.184 "docker service ls --format '{{.Name}} {{.Image}}' | grep techno"
# → techno-todo-v2-mzs9mj techno-todo:<neuer-commit>
# 3. Live testen:
curl -H "Host: techno-todo.192.168.0.184.sslip.io" http://192.168.0.184/
```
+134
View File
@@ -0,0 +1,134 @@
# Dokploy-Deployment
Diese Anleitung beschreibt **beide** Wege, die `techno-todo`-App auf Dokploy zu deployen:
1. **[Der native Dokploy-Weg](#der-native-dokploy-weg)** — wie man es „richtig" über die
Dokploy-UI/API macht (für zukünftige Apps empfohlen).
2. **[Was tatsächlich gemacht wurde](#was-tatsächlich-gemacht-wurde)** — der POC-Verlauf
inkl. aller Workarounds, die wegen kaputter Dokploy-Zustände nötig waren.
---
## Voraussetzungen
- Dokploy-Instanz: `https://dokploy.kgessner.de` (intern `192.168.0.184`)
- Gitea: `https://git.kgessner.de` (läuft in einem K8s/Talos-Cluster auf Proxmox)
- Docker-Swarm auf dem Dokploy-Host aktiv
---
## Der native Dokploy-Weg
### 1. Git-Provider (Gitea) verbinden
In der Dokploy-UI unter **Settings → Git Providers → Gitea**:
- **Gitea URL:** `https://git.kgessner.de`
- **Access Token:** ein Gitea-PAT mit Scope `repo`, `repo:status`, `read:user`, `read:org`
> Alternativ per API:
> ```bash
> curl -X POST "https://dokploy.kgessner.de/api/git.provider.gitea.create" \
> -H "x-api-key: $DOKPLOY_TOKEN" \
> -H "Content-Type: application/json" \
> -d '{"giteaUrl":"https://git.kgessner.de","accessToken":"<GITEA_TOKEN>"}'
> ```
### 2. Projekt anlegen
**Projects → Create Project** → Name `techno-todo`.
### 3. Application anlegen (Git-Source)
**New Application** mit diesen Einstellungen:
| Feld | Wert |
|------|------|
| Application Name | `techno-todo` |
| Build Type | `Dockerfile` |
| Provider | Gitea |
| Repository | `LuiiCode/techno-todo` |
| Branch | `main` |
| Dockerfile Path | `./Dockerfile` |
| Port | `3000` |
### 4. (Optional) Datenbank provisionieren
Falls du die PostgreSQL-Variante nutzt (Branch `v1-postgres`), provisioniere eine DB:
**New Database → PostgreSQL** → Name `techno-db`. Dokploy erzeugt automatisch einen
Docker-Service und stellt die Verbindungsdaten als Env-Variablen bereit.
> **Achtung (POC-Lesson):** Die App braucht dann `DATABASE_URL`, die auf den
> DB-Service-Namen zeigt (z.B. `techno-db-xxxx`), **nicht** `localhost`.
### 5. Domain konfigurieren
Unter **Application → Domains**:
- **Host:** `techno-todo.192.168.0.184.sslip.io`
- **Port:** `3000`
- **HTTPS:** HTTP (für LAN-Test ausreichend)
Dokploy erzeugt automatisch die Traefik-Routing-Konfiguration unter
`/etc/dokploy/traefik/dynamic/<app>.yml`.
### 6. Auto-Deploy aktivieren
Dokploy hat einen nativen Auto-Deploy: In den **Application-Einstellungen** unter
**Deployments** den Schalter für **Automatic Deployments** aktivieren und den
**Webhook-URL** aus der UI kopieren. Diesen URL in Gitea als Webhook eintragen:
**Gitea → Repo → Settings → Webhooks → Add Webhook (Gitea)**:
- **URL:** `<dokploy-webhook-url>` (aus Dokploy-UI)
- **Content Type:** `application/json`
- **Secret:** (optional)
- **Events:** Push
> **Wichtig:** Der native Webhook braucht einen **gültigen Dokploy-API-Key**. Ist der Key
> abgelaufen, funktioniert der Auto-Deploy nicht (genau das ist beim POC passiert).
---
## Was tatsächlich gemacht wurde
Der POC lief **nicht** sauber über den nativen Weg, weil mehrere Dokploy-Zustände kaputt
waren. Hier der tatsächliche Verlauf:
### Problem 1: DB-Service existierte nicht
Die App (PostgreSQL-Version) crashte im Loop:
- v1: `ECONNREFUSED ::1:5432` (zeigte fälschlich auf localhost)
- v2: `ENOTFOUND techno-db-8u3yoi` (falscher Service-Name)
**Lösung:** App auf **SQLite** umgebaut (kein externer DB-Service nötig).
### Problem 2: Traefik-Port falsch
Traefik routete auf `:3000`, aber Nginx (Test-App) hört auf `:80` → `Bad Gateway`.
**Lösung:** Port in `/etc/dokploy/traefik/dynamic/<app>.yml` korrigiert + Traefik reload:
```bash
sed -i 's|:3000|:80|' /etc/dokploy/traefik/dynamic/<app>.yml
docker kill -s HUP dokploy-traefik
```
### Problem 3: Dokploy-API-Key abgelaufen
Alle API-Aufrufe gaben `Unauthorized` / `Invalid API key`. Der Deploy-Worker loggte
`Error verifying API key`.
**Lösung:** Neuen Key aus der Dokploy-DB extrahiert (siehe TROUBLESHOOTING).
### Problem 4: Deploy-Worker startete keine Container
Deployments wurden angenommen, aber Container wurden nie gebaut/gestartet.
**Lösung:** Manuell per Docker gebaut + deployed (später durch die Auto-Deploy-Pipeline
ersetzt — siehe `AUTODEPLOY.md`).
### Finaler Zustand
Der Service läuft als Docker-Swarm-Service `techno-todo-v2-mzs9mj` mit dem SQLite-Image,
erreichbar unter `http://techno-todo.192.168.0.184.sslip.io/`.
+130
View File
@@ -0,0 +1,130 @@
# Troubleshooting — alle POC-Fehler und Lösungen
Chronologische Liste aller Probleme, die beim Dokploy-POC aufgetreten sind.
---
## 1. App crasht: `ECONNREFUSED ::1:5432` / `127.0.0.1:5432`
**Symptom:** Docker-Container startet und stirbt sofort, Log zeigt
`Failed to init DB: AggregateError [ECONNREFUSED]`.
**Ursache:** Die App (PostgreSQL-Version) nutzte `localhost` als DB-Host, aber die DB
läuft als separater Docker-Service, nicht im selben Container.
**Lösung:** `DATABASE_URL` auf den Service-Namen zeigen lassen, **oder** (POC-Entscheidung)
auf SQLite umsteigen — keine externe DB nötig.
---
## 2. App crasht: `ENOTFOUND techno-db-8u3yoi`
**Symptom:** DNS-Lookup des DB-Hostnames schlägt fehl.
**Ursache:** Der `techno-db` Docker-Service existierte gar nicht — Dokploy hat ihn nie
erstellt, obwohl die API-Provisionierung „OK" meldete.
**Lösung:** SQLite-Umstellung (siehe oben).
---
## 3. `Bad Gateway` trotz laufender App
**Symptom:** Intern läuft die App, aber über die Domain kommt `Bad Gateway`.
**Ursache:** Traefik routete auf Port `:3000`, die App (bzw. `nginx:alpine`) lauscht aber
auf `:80`. Dokploy setzt standardmäßig `:3000`.
**Diagnose:**
```bash
docker exec dokploy-traefik wget -qO- http://<service>:80/ # funktioniert
docker exec dokploy-traefik wget -qO- http://<service>:3000/ # connection refused
```
**Lösung:** Traefik-Config korrigieren:
```bash
# Config liegt auf dem Host (Bind-Mount)
sed -i 's|http://<service>:3000|http://<service>:80|' /etc/dokploy/traefik/dynamic/<app>.yml
docker kill -s HUP dokploy-traefik # reload
```
---
## 4. Dokploy-API: `Unauthorized` / `Invalid API key`
**Symptom:** Alle API-Calls geben 401, Deploy-Worker loggt `Error verifying API key`.
**Ursache:** Der gespeicherte API-Key war abgelaufen/ungültig.
**Diagnose + Lösung:** Key direkt aus der Dokploy-Postgres-DB lesen:
```bash
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy \
-c "SELECT name, key, start FROM apikey;"
```
**Hinweis:** Der `key`-Wert in der DB ist gehasht (BetterAuth). Für neue Keys: über die
Dokploy-UI unter **Settings → API Keys** neu erzeugen.
---
## 5. Deploy-Worker startet keine Container
**Symptom:** Deployment wird angenommen, aber kein Container wird gebaut/gestartet.
Worker-Logs zeigen `service <name> not found`.
**Ursache:** Services wurden manuell umbenannt/gelöscht → Dokploy ist **out-of-sync** mit
dem tatsächlichen Docker-Zustand.
**Lösung:** Manuell bauen + deployen (später durch die Auto-Deploy-Pipeline ersetzt):
```bash
cd /opt/repos/techno-todo
docker build -t techno-todo:<commit> .
docker service update --force --image techno-todo:<commit> <service-name>
```
---
## 6. Gitea-Webhook löst nicht zuverlässig aus
**Symptom:** Manche Pushes triggern den Deploy, andere nicht.
**Ursache:** Gitea läuft in einem **K8s/Talos-Cluster** — ausgehende Verbindungen zu
internen `192.168.0.x`-Adressen sind unzuverlässig (CNI/NAT).
**Lösung:** Cron-Polling-Fallback (idempotentes Deploy-Skript alle 2 Min). Siehe
`AUTODEPLOY.md`.
---
## 7. Race-Condition: alter Commit wird deployed
**Symptom:** Webhook feuert, aber das Deploy holt noch den alten Commit.
**Ursache:** `git fetch` läuft, bevor Gitea den Push vollständig ins Git-Backend
integriert hat.
**Lösung:** Entweder `sleep 3` vor `git fetch`, oder (besser) idempotentes Skript — der
nächste Cron-Lauf holt den verpassten Commit automatisch nach.
---
## Nützliche Diagnose-Befehle
```bash
# Docker-Swarm-Services
docker service ls
docker service logs --tail 50 <service>
# Traefik-Routing
docker exec dokploy-traefik wget -qO- http://localhost:8080/api/http/routers
# Dokploy-Logs (Deploy-Worker)
docker service logs --tail 50 dokploy 2>&1 | grep -iE 'worker|error|deploy'
# Dokploy-DB direkt abfragen
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy -c "\dt"
# Token aus DB extrahieren (Gitea)
docker exec dokploy-postgres.1.<id> psql -U dokploy -d dokploy \
-c "SELECT access_token FROM gitea;"
```