diff --git a/README.md b/README.md new file mode 100644 index 0000000..394c7c6 --- /dev/null +++ b/README.md @@ -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`.) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..1429ac0 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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/.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. psql -U dokploy -d dokploy -c "\dt" +``` + +## sslip.io — der Magic-DNS-Trick + +`.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. diff --git a/docs/AUTODEPLOY.md b/docs/AUTODEPLOY.md new file mode 100644 index 0000000..2704a9f --- /dev/null +++ b/docs/AUTODEPLOY.md @@ -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/` +2. Deploy-Skript anpassen (Repo-Dir + Service-Name) → `/opt/deploy-.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: + +# 3. Live testen: +curl -H "Host: techno-todo.192.168.0.184.sslip.io" http://192.168.0.184/ +``` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..6cd944d --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -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":""}' +> ``` + +### 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/.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:** `` (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/.yml` korrigiert + Traefik reload: +```bash +sed -i 's|:3000|:80|' /etc/dokploy/traefik/dynamic/.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/`. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..c0af8e1 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -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://:80/ # funktioniert +docker exec dokploy-traefik wget -qO- http://:3000/ # connection refused +``` + +**Lösung:** Traefik-Config korrigieren: +```bash +# Config liegt auf dem Host (Bind-Mount) +sed -i 's|http://:3000|http://:80|' /etc/dokploy/traefik/dynamic/.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. 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 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: . +docker service update --force --image techno-todo: +``` + +--- + +## 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 + +# 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. psql -U dokploy -d dokploy -c "\dt" + +# Token aus DB extrahieren (Gitea) +docker exec dokploy-postgres.1. psql -U dokploy -d dokploy \ + -c "SELECT access_token FROM gitea;" +```