# DEPLOY.md — CheckPoint Ehrenamt auf einen EU-VPS > Ziel: App unter **gunicorn** hinter **nginx**, **HTTPS** per Let's Encrypt, > Secrets aus der Umgebung, **SQLite + Uploads persistent** in `instance/`, > tägliches **DB-Backup**. Befehle für die **Fish-Shell**, auf dem VPS auszuführen. > > Platzhalter überall ersetzen: Domain `checkpoint.example.org`, ggf. Pfad > `/opt/checkpoint`. Annahme: frischer Debian/Ubuntu-VPS in der EU, Domain zeigt > per A/AAAA-Record auf die VPS-IP, du hast einen sudo-fähigen Login. --- ## 1 · Systempakete ```fish sudo apt update sudo apt install -y python3 python3-venv python3-pip nginx sqlite3 git certbot python3-certbot-nginx ``` ## 2 · Dienst-Nutzer + Verzeichnis ```fish sudo useradd --system --create-home --home-dir /opt/checkpoint --shell /usr/sbin/nologin checkpoint sudo mkdir -p /opt/checkpoint ``` ## 3 · Code holen ```fish sudo git clone /opt/checkpoint # oder per scp/rsync hochladen. Danach Eigentümer setzen: sudo chown -R checkpoint:checkpoint /opt/checkpoint ``` ## 4 · Virtualenv + Abhängigkeiten ```fish cd /opt/checkpoint sudo -u checkpoint python3 -m venv .venv sudo -u checkpoint .venv/bin/pip install --upgrade pip sudo -u checkpoint .venv/bin/pip install -r requirements.txt ``` ## 5 · Secrets (`.env`) `.env` liegt unter `/opt/checkpoint/.env`, ist **nicht** im Git und wird von systemd geladen. Aus der Vorlage erzeugen und ein echtes Secret eintragen: ```fish cd /opt/checkpoint sudo -u checkpoint cp .env.example .env # Secret erzeugen und einsetzen: set SECRET (sudo -u checkpoint .venv/bin/python -c "import secrets; print(secrets.token_urlsafe(48))") sudo -u checkpoint sed -i "s|^SECRET_KEY=.*|SECRET_KEY=$SECRET|" .env sudo chmod 600 .env ``` `.env` sollte enthalten: `CP_ENV=production` und `SECRET_KEY=`. ## 6 · Datenbank anlegen, Team seeden, Admin erstellen ```fish cd /opt/checkpoint # DB-Tabellen + erstes Team (CheckPoint) + die zwei Chat-Kanäle (idempotent): sudo -u checkpoint --preserve-env=CP_ENV env CP_ENV=production .venv/bin/python seed.py # Ersten Admin anlegen (Nutzername Anzeigename Passwort): sudo -u checkpoint env CP_ENV=production SECRET_KEY=$SECRET \ .venv/bin/flask --app wsgi create-admin admin "Team-Leitung" "EinStarkesPasswort" ``` > Hinweis: DB (`instance/checkpoint.sqlite`) und Uploads (`instance/uploads/`) > liegen unter `/opt/checkpoint/instance/` und bleiben über Neustarts/Updates > erhalten. Nur dieses Verzeichnis ist schreibbar (systemd `ReadWritePaths`). ## 7 · gunicorn als systemd-Dienst ```fish sudo cp /opt/checkpoint/deploy/checkpoint.service /etc/systemd/system/checkpoint.service sudo systemctl daemon-reload sudo systemctl enable --now checkpoint sudo systemctl status checkpoint --no-pager ``` Test (lokal auf dem VPS): `curl -I http://127.0.0.1:8000` sollte eine Antwort liefern (Redirect zur Anmeldung ist ok). ## 8 · nginx als Reverse Proxy ```fish sudo cp /opt/checkpoint/deploy/nginx-checkpoint.conf /etc/nginx/sites-available/checkpoint sudo sed -i "s/checkpoint.example.org/DEINE-DOMAIN/" /etc/nginx/sites-available/checkpoint sudo ln -sf /etc/nginx/sites-available/checkpoint /etc/nginx/sites-enabled/checkpoint sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx ``` `client_max_body_size 12m` ist gesetzt (passt zum 10-MB-Upload-Limit der App). Statische Dateien (CSS/JS/Schriften) liefert nginx direkt; Uploads/Dokumente laufen bewusst **nicht** über nginx, sondern zugriffsgeschützt über die App. ## 9 · HTTPS (Let's Encrypt) ```fish sudo certbot --nginx -d DEINE-DOMAIN ``` certbot ergänzt den 443-Block, richtet die Weiterleitung 80→443 ein und erneuert automatisch (Timer `certbot.timer`). Erst **nach** HTTPS greifen die `Secure`-Cookies sinnvoll — `CP_ENV=production` setzt sie bereits voraus. ## 10 · Tägliches DB-Backup ```fish sudo chmod +x /opt/checkpoint/deploy/backup-db.sh sudo crontab -u checkpoint -l 2>/dev/null | cat - <(echo "30 3 * * * /opt/checkpoint/deploy/backup-db.sh") | sudo crontab -u checkpoint - ``` Sichert täglich 03:30 nach `/opt/checkpoint/backups/` (Online-`.backup`, also auch im laufenden Betrieb konsistent) und hält 14 Tage vor. Backups liegen außerhalb von Git. Tipp: zusätzlich regelmäßig vom VPS wegkopieren (anderer Ort). ## 11 · Updates einspielen ```fish cd /opt/checkpoint sudo -u checkpoint git pull sudo -u checkpoint .venv/bin/pip install -r requirements.txt sudo systemctl restart checkpoint ``` Neue Tabellen legt die App beim Start via `create_all()` an. **Achtung:** echte Schema-*Änderungen* an bestehenden Tabellen macht SQLAlchemy so nicht — dafür vorher Backup ziehen und ggf. manuell migrieren. --- ## Checkliste (Deliverables Session 15) - [ ] App läuft unter gunicorn hinter nginx → Schritte 7–8 - [ ] HTTPS aktiv → Schritt 9 - [ ] Secrets über Umgebungsvariablen, nicht im Code → Schritt 5 (`.env`, systemd `EnvironmentFile`) - [ ] DB + Uploads persistent, einfaches DB-Backup eingerichtet → `instance/` + Schritt 10 ## Fehlersuche - Dienst-Logs: `sudo journalctl -u checkpoint -e` - nginx-Logs: `sudo tail -f /var/log/nginx/error.log` - 413 beim Upload → `client_max_body_size` in nginx prüfen (muss ≥ 10 MB sein). - 500 direkt nach Deploy → meist fehlendes/falsches `SECRET_KEY` in `.env` (bei `CP_ENV=production` bricht die App ohne `SECRET_KEY` bewusst ab). - „database is locked" → sollte dank WAL + busy_timeout nicht auftreten; sonst Worker-Zahl in `gunicorn.conf.py` senken. ```