154 lines
5.4 KiB
Markdown
154 lines
5.4 KiB
Markdown
# 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 <DEIN_REPO_URL> /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=<lang & zufällig>`.
|
||
|
||
## 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.
|
||
```
|