diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..349288b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.env +.venv +venv +env +instance/ +__pycache__ +*.py[cod] +*.egg-info +.pytest_cache +.mypy_cache +.ruff_cache +.DS_Store +.git +reference/ diff --git a/04_VPS_Serverstruktur_Prompt.md b/04_VPS_Serverstruktur_Prompt.md new file mode 100644 index 0000000..43622f8 --- /dev/null +++ b/04_VPS_Serverstruktur_Prompt.md @@ -0,0 +1,106 @@ +# VPS Server Structure — Context Prompt for Deployments + +*Paste this at the start of any new chat where you're deploying or configuring something on the VPS.* + +--- + +## Machine + +IONOS VPS, type VPS 6-8-240 (6 vCore, 8 GB RAM, 240 GB NVMe) +OS: Debian 13 (trixie) +User: `patsy` +Domain: `bujour.de` / `bujour.info` + +## Directory layout + +``` +/home/patsy/ +├── stacks/ # Docker Compose stacks for infrastructure +│ ├── caddy/ # Reverse proxy +│ │ ├── compose.yaml +│ │ └── Caddyfile +│ └── forgejo/ # Git server +└── apps/ # Application source / runtime dirs + └── checkpoint/ # CheckPoint Ehrenamt app +``` + +## Running containers + +| Container | Image | Ports | Network | +|------------|-------------------------------|------------------------------|---------| +| caddy | caddy:2 | 80, 443, 443/udp | proxy | +| forgejo | codeberg.org/forgejo/forgejo:10 | 3000 (internal), 2222 (SSH) | proxy | +| checkpoint | checkpoint-checkpoint | 8000 (internal) | proxy | + +## Networking + +- External Docker network: `proxy` — all containers that need to be reachable via Caddy must join this network +- Caddy is the only container with public-facing ports (80/443) +- App containers expose ports internally only (no `host:container` mapping needed) + +## Caddy routing (~/stacks/caddy/Caddyfile) + +``` +git.bujour.de { + reverse_proxy forgejo:3000 +} + +cpe.bujour.info { + reverse_proxy checkpoint:8000 +} +``` + +## Adding a new app — the pattern + +1. Create `~/apps//` for app source/config +2. Create `~/stacks//compose.yaml` — join network `proxy`, expose port internally only +3. Add a block to `~/stacks/caddy/Caddyfile`: `subdomain.domain.tld { reverse_proxy : }` +4. Reload Caddy: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` +5. Restart new stack: `cd ~/stacks/ && docker compose up -d` + +## Caddy compose (~/stacks/caddy/compose.yaml) + +```yaml +services: + caddy: + image: caddy:2 + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: + - proxy + +volumes: + caddy_data: + caddy_config: + +networks: + proxy: + external: true +``` + +## Shell & scripting rules + +- Interactive: Bash + fzf (fuzzy history: Ctrl+R, files: Ctrl+T, dirs: Alt+C) +- All scripts touching the VPS: Bash/POSIX only — never Fish +- `sudo` required for most system commands (patsy is not root) +- `ufw` requires `sudo` — `/usr/sbin/` not in default PATH by design + +## Firewall + +- UFW active on VPS +- IONOS hardware firewall also configured (both must be opened for new ports) +- IONOS web console = emergency recovery path — always keep it accessible + +## Hard rules (learned the hard way) + +- Always seed the DB and create the first admin user before considering a deploy done +- Always verify SSH key access before applying any hardening +- New ports need opening in **both** UFW and the IONOS firewall panel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81a3d80 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["gunicorn", "-c", "gunicorn.conf.py", "wsgi:app"] diff --git a/VPS_DEPLOY.md b/VPS_DEPLOY.md new file mode 100644 index 0000000..9619de1 --- /dev/null +++ b/VPS_DEPLOY.md @@ -0,0 +1,338 @@ +# VPS_DEPLOY.md — Projekt-Hub auf dem VPS deployen (Docker + Caddy) + +Deployment-Anleitung für den IONOS VPS (Debian 13, `patsy`, Docker + Caddy). +Diese Anleitung ersetzt `DEPLOYMENT.md` für diesen Server. + +**Zielarchitektur:** +``` +Internet → Caddy (Port 80/443, TLS automatisch) → projekt-hub:8000 (Docker) → SQLite (Volume) +``` + +**Voraussetzungen:** +- SSH-Zugang als `patsy` funktioniert +- Docker und die `proxy`-Netzwerk-Infrastruktur laufen (`caddy` + `proxy`-Netz) +- Code ist im lokalen Repo fertig und committed + +--- + +## 1. Code auf den Server bringen + +### Option A: Über Forgejo (git.bujour.de) — empfohlen + +Zuerst lokal ins Forgejo pushen: + +```bash +# Einmalig: Remote hinzufügen (falls noch nicht geschehen) +git remote add forgejo ssh://git@git.bujour.de:2222/patsy/projekt-hub.git + +git push forgejo main +``` + +Dann auf dem VPS: + +```bash +ssh patsy@ +mkdir -p ~/apps +git clone ssh://git@git.bujour.de:2222/patsy/projekt-hub.git ~/apps/projekt-hub +``` + +### Option B: Dateien direkt hochladen + +```bash +# Lokal ausführen: +scp -r /pfad/zum/projekt-hub patsy@:~/apps/projekt-hub +``` + +--- + +## 2. Docker-Image bauen + +```bash +cd ~/apps/projekt-hub +docker build -t projekt-hub:latest . +``` + +Dauert beim ersten Mal ~2 Minuten (Dependencies werden heruntergeladen und gecacht). +Danach prüfen, dass das Image existiert: + +```bash +docker images | grep projekt-hub +``` + +--- + +## 3. Verzeichnisse anlegen + +```bash +mkdir -p ~/apps/projekt-hub/instance +mkdir -p ~/stacks/projekt-hub +``` + +--- + +## 4. `.env`-Datei anlegen + +```bash +nano ~/apps/projekt-hub/.env +``` + +Inhalt (Werte anpassen): + +```ini +FLASK_ENV=production +SECRET_KEY= + +# Datenbank (relative sqlite-Namen werden in instance/ aufgelöst) +DATABASE_URL=sqlite:///hub.sqlite + +# Auth — nur diese E-Mail darf sich einloggen +ALLOWED_EMAILS=deine@adresse.com + +# Login-Code-Einstellungen +LOGIN_CODE_TTL_MINUTES=10 +LOGIN_CODE_RATE_LIMIT=5 per hour + +# Rate-Limiting: memory:// reicht für Einzelnutzer / einen Worker +RATELIMIT_STORAGE_URI=memory:// + +# SMTP (siehe Abschnitt 7) +MAIL_SERVER=smtp.infomaniak.com +MAIL_PORT=587 +MAIL_USE_TLS=true +MAIL_USE_SSL=false +MAIL_USERNAME=deine@adresse.com +MAIL_PASSWORD= +MAIL_DEFAULT_SENDER=deine@adresse.com + +# Cookies: Secure-Flag (HTTPS läuft über Caddy) +SESSION_COOKIE_SECURE=true + +# SERVER_NAME leer lassen — Caddy übernimmt die Domain +SERVER_NAME= +``` + +Secret Key generieren: + +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +Datei absichern: + +```bash +chmod 600 ~/apps/projekt-hub/.env +``` + +--- + +## 5. Datenbank initialisieren + +Die Migration einmalig in einem kurzlebigen Container ausführen: + +```bash +docker run --rm \ + --env-file /home/patsy/apps/projekt-hub/.env \ + -v /home/patsy/apps/projekt-hub/instance:/app/instance \ + projekt-hub:latest \ + flask db upgrade +``` + +Prüfen, dass die SQLite-Datei angelegt wurde: + +```bash +ls -lh ~/apps/projekt-hub/instance/ +# → hub.sqlite sollte erscheinen +``` + +--- + +## 6. Compose-Stack einrichten und starten + +`compose.yaml` aus dem Repo-Template kopieren: + +```bash +cp ~/apps/projekt-hub/deploy/compose.yaml ~/stacks/projekt-hub/compose.yaml +``` + +Stack starten: + +```bash +cd ~/stacks/projekt-hub +docker compose up -d +``` + +Container-Log prüfen (sollte "Listening at: http://0.0.0.0:8000" zeigen): + +```bash +docker logs projekt-hub +``` + +--- + +## 7. Caddy konfigurieren + +Einen Block zur Caddyfile hinzufügen: + +```bash +nano ~/stacks/caddy/Caddyfile +``` + +Block anfügen (Subdomain nach Wunsch wählen, z.B. `hub.bujour.de`): + +``` +hub.bujour.de { + reverse_proxy projekt-hub:8000 +} +``` + +Caddy neu laden — kein Neustart nötig, Caddy fetcht das TLS-Zertifikat automatisch: + +```bash +docker exec caddy caddy reload --config /etc/caddy/Caddyfile +``` + +Im Browser `https://hub.bujour.de` öffnen — die Login-Seite sollte erscheinen. + +> **DNS:** Der A-Record `hub.bujour.de → ` muss im DNS-Provider gesetzt sein, +> bevor Caddy das Zertifikat holen kann. Vorher ist die App nur intern erreichbar. + +--- + +## 8. E-Mail-Versand einrichten + +### Empfehlung: externer EU-SMTP-Relay (Infomaniak, Mailjet EU) + +Am einfachsten und zuverlässigsten für Docker: ein externer SMTP-Anbieter. +SMTP-Zugangsdaten im Anbieter-Konto anlegen und in `.env` eintragen (Abschnitt 4). +Am Flask-Code ändert sich nichts — die App liest alles aus der Umgebung. + +Testmail aus einem kurzlebigen Container: + +```bash +docker run --rm \ + --env-file /home/patsy/apps/projekt-hub/.env \ + projekt-hub:latest \ + python3 -c " +from dotenv import load_dotenv +from app import create_app +from app.extensions import mail +from flask_mail import Message +app = create_app('production') +with app.app_context(): + msg = Message('Testmail Projekt-Hub', recipients=['deine@adresse.com']) + msg.body = 'Mailversand funktioniert.' + mail.send(msg) +print('Mail verschickt.') +" +``` + +### Alternative: Proton Bridge in Docker + +Falls Proton Mail Bridge gewünscht: als eigenen Stack deployen und +`MAIL_SERVER=proton-bridge` in `.env` setzen (Container-Name als Hostname). +Proton Bridge muss im selben Docker-Netzwerk oder einem gemeinsamen Netzwerk laufen. +Details: `DEPLOYMENT.md §7` (Vorsicht: `127.0.0.1` funktioniert in Docker-Containern +**nicht** als SMTP-Adresse — immer Container-Name oder externe IP verwenden). + +--- + +## 9. Abschluss-Test: Login-Flow + +1. `https://hub.bujour.de` öffnen +2. E-Mail-Adresse aus `ALLOWED_EMAILS` eingeben und absenden +3. Postfach prüfen — 6-stelliger Code sollte innerhalb weniger Sekunden ankommen +4. Code auf der Verify-Seite eingeben +5. Weiterleitung zur Projektübersicht → fertig + +--- + +## 10. Sicherheits-Checkliste + +- [ ] `.env` hat restriktive Rechte: `chmod 600 ~/apps/projekt-hub/.env` +- [ ] `FLASK_ENV=production` und `SECRET_KEY` sind gesetzt (kein Default) +- [ ] `SESSION_COOKIE_SECURE=true` ist gesetzt (HTTPS läuft via Caddy) +- [ ] UFW: Ports 22, 80, 443 offen — **kein** direkter Zugriff auf Port 8000 von außen +- [ ] IONOS-Hardware-Firewall ebenfalls prüfen (nur 22, 80, 443 erlaubt) +- [ ] `.env` ist in `.gitignore` — niemals committen + +UFW-Status prüfen: + +```bash +sudo ufw status +``` + +Port 8000 ist nur intern (Docker `proxy`-Netz) erreichbar — das ist korrekt und gewollt. + +--- + +## 11. Updates einspielen + +```bash +cd ~/apps/projekt-hub +git pull + +# Image neu bauen +docker build -t projekt-hub:latest . + +# Migrationen (falls neue vorhanden) +docker run --rm \ + --env-file /home/patsy/apps/projekt-hub/.env \ + -v /home/patsy/apps/projekt-hub/instance:/app/instance \ + projekt-hub:latest \ + flask db upgrade + +# Container neu starten +cd ~/stacks/projekt-hub +docker compose up -d --force-recreate +``` + +--- + +## 12. Backup + +Die gesamte App-Datenbank ist eine einzelne SQLite-Datei. Tägliches Backup per Cronjob: + +```bash +crontab -e +``` + +Zeile hinzufügen: + +``` +0 3 * * * sqlite3 /home/patsy/apps/projekt-hub/instance/hub.sqlite ".backup '/home/patsy/backups/hub-$(date +\%F).sqlite'" && find /home/patsy/backups -name "hub-*.sqlite" -mtime +30 -delete +``` + +Backup-Verzeichnis anlegen: + +```bash +mkdir -p ~/backups +``` + +`sqlite3 .backup` ist konsistenter als einfaches `cp` bei laufender App. + +--- + +## Anhang: Häufige Befehle + +```bash +# Logs ansehen +docker logs -f projekt-hub + +# Container neu starten +docker compose -f ~/stacks/projekt-hub/compose.yaml restart + +# Container stoppen / starten +docker compose -f ~/stacks/projekt-hub/compose.yaml down +docker compose -f ~/stacks/projekt-hub/compose.yaml up -d + +# Image neu bauen und Container ersetzen +cd ~/apps/projekt-hub && docker build -t projekt-hub:latest . && \ + cd ~/stacks/projekt-hub && docker compose up -d --force-recreate + +# Flask-Shell für Debugging +docker run --rm -it \ + --env-file /home/patsy/apps/projekt-hub/.env \ + -v /home/patsy/apps/projekt-hub/instance:/app/instance \ + projekt-hub:latest \ + flask shell +``` diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 0000000..0515877 --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,22 @@ +# Docker Compose template für ~/stacks/projekt-hub/compose.yaml auf dem VPS. +# Vollständige Anleitung: VPS_DEPLOY.md +# +# Auf den VPS kopieren: +# scp deploy/compose.yaml patsy@:~/stacks/projekt-hub/compose.yaml +# +# Pfade ggf. anpassen, falls die App nicht unter ~/apps/projekt-hub/ liegt. + +services: + projekt-hub: + image: projekt-hub:latest + container_name: projekt-hub + restart: unless-stopped + env_file: /home/patsy/apps/projekt-hub/.env + volumes: + - /home/patsy/apps/projekt-hub/instance:/app/instance + networks: + - proxy + +networks: + proxy: + external: true diff --git a/gunicorn.conf.py b/gunicorn.conf.py index c7f3272..12b7377 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -10,17 +10,16 @@ Umgebungsvariablen überschreibbar, sinnvolle Defaults für einen kleinen VPS. import multiprocessing import os -# Bind-Adresse: standardmäßig ein Unix-Socket relativ zum WorkingDirectory. -# nginx spricht denselben Socket per absoluter Pfadangabe an -# (z.B. /home/projekthub/projekt-hub/projekt-hub.sock). -bind = os.environ.get("GUNICORN_BIND", "unix:projekt-hub.sock") +# Bind-Adresse: Standard TCP-Port 8000 (Docker/Caddy-Betrieb). +# Für Unix-Socket (z.B. nginx auf demselben Host): GUNICORN_BIND=unix:projekt-hub.sock +bind = os.environ.get("GUNICORN_BIND", "0.0.0.0:8000") # Worker-Anzahl: Faustregel (2 × CPU-Kerne) + 1, per env begrenzbar. workers = int(os.environ.get("GUNICORN_WORKERS", str(multiprocessing.cpu_count() * 2 + 1))) worker_class = os.environ.get("GUNICORN_WORKER_CLASS", "sync") timeout = int(os.environ.get("GUNICORN_TIMEOUT", "30")) -# Socket-Dateirechte, damit nginx (Gruppe www-data) zugreifen kann. +# Socket-Dateirechte (nur relevant im Unix-Socket-Modus). umask = 0o007 # Logs auf stdout/stderr → landen via systemd im journal (journalctl -u projekt-hub).