364 lines
9.9 KiB
Markdown
364 lines
9.9 KiB
Markdown
# DEPLOYMENT.md — Projekt-Hub auf dem VPS installieren
|
||
|
||
Diese Anleitung beschreibt, wie die fertige Flask-App (siehe `CLAUDE.md` und
|
||
`TASKLISTE.md`) auf einem Ubuntu-VPS produktiv läuft. Zielarchitektur:
|
||
|
||
```
|
||
Internet → nginx (Port 80/443, TLS) → gunicorn (Unix-Socket) → Flask-App → SQLite
|
||
```
|
||
|
||
systemd sorgt dafür, dass die App automatisch beim Booten startet und bei einem Absturz neu startet.
|
||
|
||
Vorausgesetzt: Ubuntu-VPS mit SSH-Zugang und sudo-Rechten. Domain ist optional —
|
||
diese Anleitung funktioniert zunächst auch nur mit der Server-IP (HTTPS dann erst,
|
||
sobald eine Domain feststeht).
|
||
|
||
---
|
||
|
||
## 1. System vorbereiten
|
||
|
||
```bash
|
||
sudo apt update && sudo apt upgrade -y
|
||
sudo apt install -y python3 python3-venv python3-pip nginx git
|
||
```
|
||
|
||
Eigenen, nicht-root Nutzer für die App anlegen (falls noch nicht vorhanden):
|
||
|
||
```bash
|
||
sudo adduser projekthub
|
||
sudo usermod -aG sudo projekthub # optional, falls dieser Nutzer auch administrieren soll
|
||
```
|
||
|
||
Ab hier alles als `projekthub`-Nutzer ausführen (`su - projekthub`), außer wo
|
||
explizit `sudo` für Systemdienste nötig ist.
|
||
|
||
---
|
||
|
||
## 2. Code auf den Server bringen
|
||
|
||
Variante A — über Git (empfohlen, falls Repo z.B. auf einem privaten Git-Server
|
||
oder GitHub liegt):
|
||
|
||
```bash
|
||
cd ~
|
||
git clone <DEIN_REPO_URL> projekt-hub
|
||
cd projekt-hub
|
||
```
|
||
|
||
Variante B — Dateien direkt hochladen (z.B. von deinem Rechner aus):
|
||
|
||
```bash
|
||
scp -r projekt-hub/ projekthub@DEINE_SERVER_IP:~/projekt-hub
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Python-Umgebung einrichten
|
||
|
||
```bash
|
||
cd ~/projekt-hub
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install --upgrade pip
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
`gunicorn` und der Redis-Client sind bereits in `requirements.txt` enthalten —
|
||
kein separates Installieren nötig. Der **Redis-Server** (für Rate-Limiting bei
|
||
mehreren Workern, siehe Abschnitt 8a) ist ein Systemdienst und wird separat
|
||
installiert:
|
||
|
||
```bash
|
||
sudo apt install -y redis-server
|
||
sudo systemctl enable --now redis-server
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Konfigurationsdatei `.env` anlegen
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
nano .env
|
||
```
|
||
|
||
Wichtige Werte für die Produktion:
|
||
|
||
```ini
|
||
FLASK_ENV=production
|
||
SECRET_KEY=<langer-zufaelliger-string>
|
||
# Relativer sqlite-Name wird automatisch in den instance/-Ordner aufgelöst
|
||
# (-> instance/hub.sqlite). Keinen instance/-Pfad voranstellen.
|
||
DATABASE_URL=sqlite:///hub.sqlite
|
||
|
||
# Login
|
||
ALLOWED_EMAILS=deine@adresse.com
|
||
|
||
# Rate-Limiting über gemeinsames Backend (mehrere Worker), siehe Abschnitt 8a
|
||
RATELIMIT_STORAGE_URI=redis://localhost:6379
|
||
|
||
# Mail (siehe Abschnitt 7)
|
||
MAIL_SERVER=127.0.0.1
|
||
MAIL_PORT=1025
|
||
MAIL_USERNAME=deine@adresse.com
|
||
MAIL_PASSWORD=<bridge-oder-relay-passwort>
|
||
MAIL_USE_TLS=true
|
||
|
||
# Domain (sobald bekannt, siehe Abschnitt 9)
|
||
SERVER_NAME=
|
||
SESSION_COOKIE_SECURE=true
|
||
```
|
||
|
||
Einen zufälligen `SECRET_KEY` generieren:
|
||
|
||
```bash
|
||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||
```
|
||
|
||
`.env` darf **niemals** ins Git-Repo committen — sicherstellen, dass `.gitignore`
|
||
`.env` enthält.
|
||
|
||
---
|
||
|
||
## 5. Datenbank initialisieren
|
||
|
||
```bash
|
||
mkdir -p instance
|
||
flask db upgrade
|
||
```
|
||
|
||
Prüfen, dass `instance/hub.sqlite` existiert:
|
||
|
||
```bash
|
||
ls -la instance/
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Testlauf mit dem Flask-Dev-Server (nur zur Kontrolle)
|
||
|
||
```bash
|
||
flask run --host=0.0.0.0 --port=5000
|
||
```
|
||
|
||
Im Browser `http://DEINE_SERVER_IP:5000` aufrufen — die Login-Seite sollte
|
||
erscheinen. Danach mit `Strg+C` wieder stoppen; der Dev-Server ist **nicht** für
|
||
Dauerbetrieb gedacht.
|
||
|
||
---
|
||
|
||
## 7. E-Mail-Versand über Proton einrichten
|
||
|
||
### Option A: Proton Mail Bridge (Docker)
|
||
|
||
Die Bridge initialisiert beim Start lokale IMAP/SMTP-Server, über die die App
|
||
Mails verschicken kann. Auf dem VPS läuft sie containerisiert:
|
||
|
||
```bash
|
||
sudo apt install -y docker.io
|
||
sudo systemctl enable --now docker
|
||
|
||
docker volume create proton-bridge
|
||
docker run -d --name proton-bridge \
|
||
-v proton-bridge:/root \
|
||
-p 127.0.0.1:1025:25 \
|
||
-p 127.0.0.1:1143:143 \
|
||
spameier/proton-bridge
|
||
```
|
||
|
||
Danach einmalig in den Container einloggen und die Bridge mit den Proton-Zugangsdaten
|
||
verbinden:
|
||
|
||
```bash
|
||
docker exec -it proton-bridge bridge --cli
|
||
# im Bridge-CLI: "login" eingeben und den Anweisungen folgen
|
||
```
|
||
|
||
Die Bridge zeigt danach lokale SMTP-Zugangsdaten (Benutzername + generiertes
|
||
Passwort) an — diese in die `.env` unter `MAIL_USERNAME` / `MAIL_PASSWORD`
|
||
eintragen, `MAIL_SERVER=127.0.0.1`, `MAIL_PORT=1025`.
|
||
|
||
**Bekannte Stolpersteine:** Proton Bridge nutzt ein selbstsigniertes Zertifikat und initiiert den TLS-Handshake selbst, was bei direkter Relay-Einbindung zu Problemen führen kann. Falls der Mailversand aus Flask nicht klappt, hilft es,
|
||
Postfix als lokalen Zwischen-Relay zu betreiben, der an die Bridge weiterleitet —
|
||
oder direkt auf Option B auszuweichen.
|
||
|
||
### Option B: EU-SMTP-Relay als Fallback
|
||
|
||
Falls Option A zu instabil ist: bei einem europäischen Transactional-Mail-Anbieter
|
||
(z.B. Infomaniak, Mailjet EU) ein kostenloses/günstiges Konto anlegen, SMTP-Zugangsdaten
|
||
erhalten und in `.env` eintragen — `MAIL_SERVER`, `MAIL_PORT`, `MAIL_USERNAME`,
|
||
`MAIL_PASSWORD`, `MAIL_USE_TLS` entsprechend anpassen. **Am Flask-Code ändert sich
|
||
nichts**, da die App generisch über `Flask-Mail`/SMTP konfiguriert ist.
|
||
|
||
### Testmail verschicken
|
||
|
||
```bash
|
||
flask shell
|
||
```
|
||
```python
|
||
from app.extensions import mail
|
||
from flask_mail import Message
|
||
msg = Message("Testmail Projekt-Hub", recipients=["deine@adresse.com"])
|
||
msg.body = "Funktioniert!"
|
||
mail.send(msg)
|
||
```
|
||
|
||
---
|
||
|
||
## 8. gunicorn + systemd einrichten
|
||
|
||
Die Gunicorn-Konfiguration liegt versioniert im Repo: **`gunicorn.conf.py`**
|
||
(Worker-Anzahl, Socket, Logging). Startbefehl testen (im `.venv`):
|
||
|
||
```bash
|
||
gunicorn -c gunicorn.conf.py wsgi:app
|
||
```
|
||
|
||
Nginx an Gunicorn über einen Unix-Socket statt TCP-Port anzubinden reduziert den TCP-Overhead und ist die bevorzugte Variante, wenn beide auf demselben Host laufen. Mit `Strg+C` stoppen, danach den systemd-Service einrichten.
|
||
|
||
Die systemd-Unit liegt ebenfalls im Repo (**`deploy/projekt-hub.service`**) und
|
||
lädt die Gunicorn-Config per `-c`. Kopieren und ggf. Pfade/Nutzer anpassen:
|
||
|
||
```bash
|
||
sudo cp deploy/projekt-hub.service /etc/systemd/system/projekt-hub.service
|
||
sudo nano /etc/systemd/system/projekt-hub.service # Pfade/Nutzer prüfen
|
||
```
|
||
|
||
Die Worker-Anzahl steuert `gunicorn.conf.py` (Default: (2 × CPU-Kerne) + 1,
|
||
per env `GUNICORN_WORKERS` überschreibbar).
|
||
|
||
Service aktivieren und starten:
|
||
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable projekt-hub
|
||
sudo systemctl start projekt-hub
|
||
sudo systemctl status projekt-hub
|
||
```
|
||
|
||
---
|
||
|
||
## 8a. Redis für Rate-Limiting
|
||
|
||
Flask-Limiter (Login-Code-Anfragen) braucht bei **mehreren** gunicorn-Workern ein
|
||
gemeinsames Speicher-Backend — sonst zählt jeder Worker eigene Limits und das
|
||
Limit greift faktisch nicht. Redis wurde in Abschnitt 3 installiert; in der `.env`
|
||
verweist `RATELIMIT_STORAGE_URI` darauf:
|
||
|
||
```ini
|
||
RATELIMIT_STORAGE_URI=redis://localhost:6379
|
||
```
|
||
|
||
Damit Redis vor der App startet, in der systemd-Unit (auskommentierte Zeilen
|
||
aktivieren):
|
||
|
||
```ini
|
||
After=network.target redis-server.service
|
||
Wants=redis-server.service
|
||
```
|
||
|
||
Schnelltest, dass Redis läuft: `redis-cli ping` → `PONG`. Wer bewusst nur **einen**
|
||
Worker fährt, kann auch bei `memory://` bleiben.
|
||
|
||
Logs ansehen:
|
||
|
||
```bash
|
||
sudo journalctl -u projekt-hub -f
|
||
```
|
||
|
||
---
|
||
|
||
## 9. nginx als Reverse Proxy
|
||
|
||
Die nginx-Vorlage liegt im Repo: **`deploy/nginx/projekt-hub.conf`** (Socket-Proxy
|
||
+ Static-Auslieferung). Kopieren und `server_name` (zunächst Server-IP) anpassen:
|
||
|
||
```bash
|
||
sudo cp deploy/nginx/projekt-hub.conf /etc/nginx/sites-available/projekt-hub
|
||
sudo nano /etc/nginx/sites-available/projekt-hub # server_name / Pfade prüfen
|
||
```
|
||
|
||
Aktivieren:
|
||
|
||
```bash
|
||
sudo ln -s /etc/nginx/sites-available/projekt-hub /etc/nginx/sites-enabled/
|
||
sudo nginx -t
|
||
sudo systemctl reload nginx
|
||
```
|
||
|
||
Im Browser `http://DEINE_SERVER_IP` aufrufen — die Login-Seite sollte jetzt über
|
||
nginx ausgeliefert werden.
|
||
|
||
---
|
||
|
||
## 10. Domain & HTTPS (sobald die Domain feststeht)
|
||
|
||
1. DNS A-Record der gewählten Domain/Subdomain auf die Server-IP zeigen lassen
|
||
2. In der nginx-Config `server_name` auf die Domain ändern, `nginx -t` +
|
||
`systemctl reload nginx`
|
||
3. Certbot installieren und Zertifikat holen:
|
||
```bash
|
||
sudo apt install -y certbot python3-certbot-nginx
|
||
sudo certbot --nginx -d deine-domain.de
|
||
```
|
||
Certbot passt die nginx-Config automatisch für HTTPS + Redirect von HTTP an.
|
||
4. In `.env`: `SERVER_NAME=deine-domain.de`, `SESSION_COOKIE_SECURE=true`,
|
||
`PREFERRED_URL_SCHEME=https` setzen, danach App neu starten:
|
||
```bash
|
||
sudo systemctl restart projekt-hub
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Updates einspielen
|
||
|
||
Wenn neue Features (z.B. weitere Module) hinzukommen:
|
||
|
||
```bash
|
||
cd ~/projekt-hub
|
||
git pull # oder neue Dateien per scp hochladen
|
||
source .venv/bin/activate
|
||
pip install -r requirements.txt # falls neue Abhängigkeiten
|
||
flask db upgrade # falls neue Migrationen
|
||
sudo systemctl restart projekt-hub
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Backup
|
||
|
||
Die gesamte App-Datenbank ist eine einzelne SQLite-Datei. Einfaches Backup per
|
||
Cronjob (täglich, Beispiel):
|
||
|
||
```bash
|
||
crontab -e
|
||
```
|
||
|
||
Zeile hinzufügen:
|
||
|
||
```
|
||
0 3 * * * cp /home/projekthub/projekt-hub/instance/hub.sqlite /home/projekthub/backups/hub-$(date +\%F).sqlite
|
||
```
|
||
|
||
Vorher den Ordner anlegen: `mkdir -p ~/backups`. Da SQLite auch während Schreib-
|
||
zugriffen kopiert werden kann, ist für ein konsistentes Backup `sqlite3 hub.sqlite
|
||
".backup '/pfad/hub-$(date +%F).sqlite'"` die sauberere Variante.
|
||
|
||
> Hinweis: Der frühere JSON-Export wurde durch den **PDF-Export** ersetzt (eine
|
||
> druckbare, projektbasierte To-Do-Liste, kein Datensicherungs-Format). Die
|
||
> Datensicherung erfolgt also ausschließlich über die SQLite-Datei wie oben.
|
||
|
||
---
|
||
|
||
## 13. Sicherheits-Checkliste (kurz)
|
||
|
||
- [ ] `.env` hat restriktive Dateirechte: `chmod 600 .env`
|
||
- [ ] `FLASK_ENV=production` gesetzt (kein Debug-Modus)
|
||
- [ ] Firewall (`ufw`) aktiv, nur Ports 22, 80, 443 offen:
|
||
```bash
|
||
sudo ufw allow OpenSSH
|
||
sudo ufw allow 'Nginx Full'
|
||
sudo ufw enable
|
||
```
|
||
- [ ] SSH-Login per Passwort deaktiviert, nur Key-Auth (falls noch nicht der Fall)
|
||
- [ ] Regelmäßige `apt upgrade` einplanen (z.B. `unattended-upgrades`)
|