a639de13f8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
505 lines
14 KiB
Markdown
505 lines
14 KiB
Markdown
# TimeMaster – Deployment-Guide
|
||
|
||
Stand: 2026-05-24
|
||
|
||
---
|
||
|
||
## Infrastruktur-Übersicht
|
||
|
||
| Server | IP | Rolle |
|
||
|--------|----|-------|
|
||
| Primary | 192.168.1.137 | Produktion, Tests, Primär-DB |
|
||
| Secondary | 192.168.1.164 | Replikat / Fallback |
|
||
|
||
Beide Server laufen Ubuntu 22.04 oder 24.04 LTS (amd64). Kein Docker in Phase 1 – alle Dienste laufen nativ als systemd-Units.
|
||
|
||
---
|
||
|
||
## Voraussetzungen
|
||
|
||
Auf jedem Server müssen folgende Pakete installiert sein:
|
||
|
||
```bash
|
||
apt-get install -y \
|
||
python3 python3-venv python3-dev python3-pip \
|
||
postgresql postgresql-contrib \
|
||
redis-server \
|
||
nginx \
|
||
git curl build-essential libpq-dev
|
||
```
|
||
|
||
Node.js 20 für den Frontend-Build wird nur auf der Entwicklungsmaschine benötigt, nicht auf den Servern. Das Frontend wird lokal gebaut und als statisches `dist/`-Verzeichnis per rsync übertragen.
|
||
|
||
---
|
||
|
||
## Erstes Setup (einmalig)
|
||
|
||
Das Setup-Skript `setup_server.sh` übernimmt alle Schritte vollautomatisch:
|
||
|
||
```bash
|
||
# Auf dem Server als root
|
||
bash /opt/timemaster/setup_server.sh
|
||
```
|
||
|
||
### Was das Skript tut
|
||
|
||
**Schritt 1 – System-Pakete:**
|
||
`apt-get update && apt-get upgrade -y` sowie alle oben genannten Abhängigkeiten.
|
||
|
||
**Schritt 2 – PostgreSQL:**
|
||
|
||
```sql
|
||
CREATE ROLE timemaster LOGIN PASSWORD 'timemaster_secret_change_me';
|
||
CREATE DATABASE timemaster_db OWNER timemaster;
|
||
CREATE DATABASE timemaster_test OWNER timemaster; -- für pytest
|
||
GRANT ALL PRIVILEGES ON DATABASE timemaster_db TO timemaster;
|
||
```
|
||
|
||
Das Passwort in Produktion sofort nach Setup ändern (siehe `.env`).
|
||
|
||
**Schritt 3 – Redis:**
|
||
`systemctl enable redis-server && systemctl start redis-server`
|
||
|
||
**Schritt 4 – Python venv:**
|
||
```bash
|
||
cd /opt/timemaster/backend
|
||
python3 -m venv venv
|
||
source venv/bin/activate
|
||
pip install --upgrade pip
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
**Schritt 5 – Alembic-Migrationen:**
|
||
```bash
|
||
alembic upgrade head
|
||
```
|
||
Führt alle Migrationen von 0001 bis zur aktuellen Kopfversion aus.
|
||
|
||
**Schritt 6 – nginx:**
|
||
Legt `/etc/nginx/sites-available/timemaster` an, verlinkt nach `sites-enabled` und startet nginx.
|
||
|
||
### Manuelles Setup der .env-Datei
|
||
|
||
Vor dem ersten Start muss `/opt/timemaster/backend/.env` angelegt werden:
|
||
|
||
```bash
|
||
cp /opt/timemaster/backend/.env.example /opt/timemaster/backend/.env
|
||
nano /opt/timemaster/backend/.env
|
||
```
|
||
|
||
Pflichtfelder in Produktion (siehe nächster Abschnitt).
|
||
|
||
### systemd-Service aktivieren
|
||
|
||
```bash
|
||
cp /opt/timemaster/timemaster.service /etc/systemd/system/
|
||
systemctl daemon-reload
|
||
systemctl enable timemaster
|
||
systemctl start timemaster
|
||
```
|
||
|
||
---
|
||
|
||
## Umgebungsvariablen (.env)
|
||
|
||
Datei: `/opt/timemaster/backend/.env`
|
||
|
||
```bash
|
||
# === App ===
|
||
APP_NAME=TimeMaster
|
||
APP_ENV=production # production | development
|
||
SECRET_KEY=<min. 32 zufällige Zeichen – openssl rand -hex 32>
|
||
FRONTEND_URL=https://yourdomain.com
|
||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||
|
||
# === Datenbank ===
|
||
DATABASE_URL=postgresql+asyncpg://timemaster:<passwort>@localhost:5432/timemaster_db
|
||
|
||
# === Redis ===
|
||
REDIS_URL=redis://localhost:6379/0
|
||
|
||
# === JWT ===
|
||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||
REFRESH_TOKEN_EXPIRE_DAYS=30
|
||
|
||
# === E-Mail (Resend.com) ===
|
||
RESEND_API_KEY=re_<key>
|
||
EMAIL_FROM=noreply@yourdomain.com
|
||
EMAIL_FROM_NAME=TimeMaster
|
||
|
||
# === Erster Super-Admin (wird beim ersten Start angelegt) ===
|
||
FIRST_SUPERADMIN_EMAIL=admin@yourdomain.com
|
||
FIRST_SUPERADMIN_PASSWORD=<starkes-passwort>
|
||
```
|
||
|
||
Pflicht in Produktion:
|
||
- `SECRET_KEY` muss einmalig und zufällig sein (min. 32 Zeichen). Die Applikation verweigert den Start mit dem Default-Wert `change-me-in-production`.
|
||
- `DATABASE_URL` mit dem echten Passwort des `timemaster`-Datenbankusers.
|
||
- `RESEND_API_KEY` für ausgehende E-Mails (Einladungen, Passwort-Reset, Willkommensmails).
|
||
|
||
---
|
||
|
||
## Systemd-Service
|
||
|
||
Datei: `/etc/systemd/system/timemaster.service`
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=TimeMaster FastAPI Backend
|
||
After=network.target postgresql.service redis.service
|
||
Requires=postgresql.service redis.service
|
||
|
||
[Service]
|
||
Type=exec
|
||
User=www-data
|
||
Group=www-data
|
||
WorkingDirectory=/opt/timemaster/backend
|
||
EnvironmentFile=/opt/timemaster/backend/.env
|
||
ExecStart=/opt/timemaster/backend/venv/bin/uvicorn app.main:app \
|
||
--host 127.0.0.1 \
|
||
--port 8000 \
|
||
--workers 4 \
|
||
--log-level info \
|
||
--access-log
|
||
ExecReload=/bin/kill -HUP $MAINPID
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=timemaster
|
||
|
||
# Sicherheitsoptionen
|
||
NoNewPrivileges=yes
|
||
PrivateTmp=yes
|
||
ProtectSystem=strict
|
||
ReadWritePaths=/opt/timemaster/backend
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
Uvicorn läuft mit 4 Workern auf Port 8000, nur auf localhost. nginx leitet eingehende Anfragen weiter.
|
||
|
||
Service-Befehle:
|
||
```bash
|
||
systemctl start timemaster
|
||
systemctl stop timemaster
|
||
systemctl restart timemaster
|
||
systemctl status timemaster
|
||
journalctl -u timemaster -f # Live-Logs
|
||
journalctl -u timemaster -n 100 # Letzte 100 Zeilen
|
||
```
|
||
|
||
---
|
||
|
||
## nginx-Konfiguration
|
||
|
||
Datei: `/etc/nginx/sites-available/timemaster` (symlink nach `sites-enabled`)
|
||
|
||
```nginx
|
||
# HTTP → HTTPS Redirect
|
||
server {
|
||
listen 80;
|
||
server_name yourdomain.com www.yourdomain.com;
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name yourdomain.com www.yourdomain.com;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
# HSTS (nach agent-08 verpflichtend)
|
||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||
|
||
client_max_body_size 20M;
|
||
|
||
# API Backend
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
}
|
||
|
||
# FastAPI Swagger Docs (nur in dev-Deployments!)
|
||
location /docs {
|
||
proxy_pass http://127.0.0.1:8000/docs;
|
||
}
|
||
location /openapi.json {
|
||
proxy_pass http://127.0.0.1:8000/openapi.json;
|
||
}
|
||
|
||
# React Frontend (SPA – alle Routen über index.html)
|
||
location / {
|
||
root /opt/timemaster/frontend/dist;
|
||
index index.html;
|
||
try_files $uri $uri/ /index.html;
|
||
expires 1d;
|
||
add_header Cache-Control "public, must-revalidate";
|
||
}
|
||
|
||
# Statische Backend-Uploads
|
||
location /static/ {
|
||
alias /opt/timemaster/backend/static/;
|
||
expires 7d;
|
||
}
|
||
}
|
||
```
|
||
|
||
TLS-Zertifikat mit Let's Encrypt:
|
||
```bash
|
||
apt-get install certbot python3-certbot-nginx
|
||
certbot --nginx -d yourdomain.com -d www.yourdomain.com
|
||
```
|
||
|
||
Nach nginx-Konfigurationsänderungen:
|
||
```bash
|
||
nginx -t && systemctl reload nginx
|
||
```
|
||
|
||
---
|
||
|
||
## Deployment-Workflow (reguläre Updates)
|
||
|
||
Der gesamte Deployment-Prozess ist in `update.sh` automatisiert.
|
||
|
||
### Vollständiges Deployment
|
||
|
||
```bash
|
||
cd /home/sysops/Dokumente/Scripte/timemaster
|
||
./update.sh
|
||
```
|
||
|
||
Führt alle Schritte für beide Server durch:
|
||
1. Tests auf Server 137 (pytest -x -q)
|
||
2. Frontend lokal bauen (npm run build)
|
||
3. `git pull --ff-only origin main` auf Server(n)
|
||
4. Alembic: `alembic upgrade head`
|
||
5. Service: `systemctl restart timemaster`
|
||
6. Health-Check: `curl http://localhost:8000/health` (3 Versuche)
|
||
7. Frontend-Dist per rsync übertragen
|
||
|
||
### Optionen
|
||
|
||
```bash
|
||
./update.sh --no-tests # Tests überspringen (schneller, nur im Notfall)
|
||
./update.sh --no-frontend # Frontend-Build überspringen (nur Backend-Änderungen)
|
||
./update.sh --server 137 # Nur Primary
|
||
./update.sh --server 164 # Nur Secondary
|
||
./update.sh --dry-run # Alle Befehle zeigen, nichts ausführen
|
||
```
|
||
|
||
### Manueller Deploy (ohne update.sh)
|
||
|
||
```bash
|
||
# 1. Tests
|
||
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && pytest -x -q"
|
||
|
||
# 2. Frontend lokal bauen
|
||
cd /home/sysops/Dokumente/Scripte/timemaster/frontend
|
||
npm run build
|
||
|
||
# 3. Code auf Server synchronisieren
|
||
ssh root@192.168.1.137 "cd /opt/timemaster && git pull --ff-only origin main"
|
||
|
||
# 4. Migrationen
|
||
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic upgrade head"
|
||
|
||
# 5. Service neustarten
|
||
ssh root@192.168.1.137 "systemctl restart timemaster"
|
||
|
||
# 6. Frontend-Dist synchronisieren
|
||
rsync -avz --delete \
|
||
/home/sysops/Dokumente/Scripte/timemaster/frontend/dist/ \
|
||
root@192.168.1.137:/opt/timemaster/frontend/dist/
|
||
|
||
# 7. Logs prüfen
|
||
ssh root@192.168.1.137 "journalctl -u timemaster -n 50"
|
||
```
|
||
|
||
---
|
||
|
||
## Monitoring und Logs
|
||
|
||
### Applikations-Logs
|
||
|
||
```bash
|
||
# Live-Stream
|
||
ssh root@192.168.1.137 "journalctl -u timemaster -f"
|
||
|
||
# Letzte 100 Zeilen
|
||
ssh root@192.168.1.137 "journalctl -u timemaster -n 100 --no-pager"
|
||
|
||
# Fehler der letzten Stunde
|
||
ssh root@192.168.1.137 "journalctl -u timemaster --since='1 hour ago' -p err"
|
||
```
|
||
|
||
### Health-Endpoint
|
||
|
||
```bash
|
||
curl https://yourdomain.com/health
|
||
# → { "status": "ok", "database": "ok", "redis": "ok" }
|
||
```
|
||
|
||
### Service-Status
|
||
|
||
```bash
|
||
ssh root@192.168.1.137 "systemctl status timemaster"
|
||
ssh root@192.168.1.137 "systemctl status nginx"
|
||
ssh root@192.168.1.137 "systemctl status postgresql"
|
||
ssh root@192.168.1.137 "systemctl status redis-server"
|
||
```
|
||
|
||
---
|
||
|
||
## Backup-Strategie
|
||
|
||
### PostgreSQL-Dump
|
||
|
||
```bash
|
||
# Vollständiger Dump (täglich per cron)
|
||
ssh root@192.168.1.137 "pg_dump -U timemaster timemaster_db | gzip > /opt/backups/timemaster_$(date +%Y%m%d).sql.gz"
|
||
|
||
# Wiederherstellung
|
||
ssh root@192.168.1.137 "gunzip -c /opt/backups/timemaster_20260524.sql.gz | psql -U timemaster timemaster_db"
|
||
```
|
||
|
||
Empfehlung: täglicher pg_dump-Cron + Upload auf externen S3-kompatiblen Speicher. Backup 30 Tage aufbewahren.
|
||
|
||
### Frontend-Dist
|
||
|
||
Das `dist/`-Verzeichnis kann jederzeit aus dem lokalen Build reproduziert werden und muss nicht separat gesichert werden.
|
||
|
||
---
|
||
|
||
## Rollback-Verfahren
|
||
|
||
### Code-Rollback
|
||
|
||
```bash
|
||
# Auf Server: bestimmten Commit auschecken
|
||
ssh root@192.168.1.137 "cd /opt/timemaster && git log --oneline -10"
|
||
ssh root@192.168.1.137 "cd /opt/timemaster && git checkout <commit-hash>"
|
||
ssh root@192.168.1.137 "systemctl restart timemaster"
|
||
```
|
||
|
||
### Alembic-Rollback
|
||
|
||
```bash
|
||
# Eine Version zurück
|
||
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic downgrade -1"
|
||
|
||
# Auf bestimmte Version zurück
|
||
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic downgrade 0023"
|
||
```
|
||
|
||
Achtung: Datenverlust möglich, wenn die `downgrade()`-Funktion Spalten löscht. Vor dem Downgrade immer einen pg_dump anlegen.
|
||
|
||
### Alembic-Diagnosebefehle
|
||
|
||
```bash
|
||
# Aktuelle Migration-Version
|
||
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic current"
|
||
|
||
# Migrationsverlauf
|
||
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic history --verbose"
|
||
```
|
||
|
||
---
|
||
|
||
## Zwei-Server-Setup
|
||
|
||
Server 137 ist der Primary. Server 164 ist das Fallback/Replikat.
|
||
|
||
Beide Server beziehen den Code über `git pull` aus demselben Gitea-Repository (`gitea.perlbach24.de/scripte/timemaster.git`). Jeder Server hat seine eigene PostgreSQL-Instanz. Es gibt keine automatische Replikation zwischen den Datenbanken – bei Failover muss manuell ein pg_dump vom Primary wiederhergestellt werden.
|
||
|
||
Update-Reihenfolge:
|
||
1. Tests immer nur auf Server 137 ausführen (update.sh-Default)
|
||
2. Migration zuerst auf 137, dann auf 164
|
||
3. Service-Restart auf 137, dann auf 164
|
||
|
||
---
|
||
|
||
## CalDAV-Konfiguration
|
||
|
||
TimeMaster ist ein **reiner CalDAV-Client** – es stellt keinen eigenen CalDAV-Server bereit. Genehmigte Abwesenheiten werden als iCal-Events per HTTP PUT in einen externen Kalenderserver (typischerweise Nextcloud) geschrieben.
|
||
|
||
### Funktionsweise
|
||
|
||
```
|
||
TimeMaster ──PUT/DELETE──► Nextcloud CalDAV
|
||
◄───────────── (kein eingehender Traffic von Nextcloud)
|
||
```
|
||
|
||
- Kein Kalender-Client (Thunderbird, Apple Calendar etc.) kann sich mit TimeMaster verbinden.
|
||
- TimeMaster kann nicht als CalDAV-Relay oder -Proxy konfiguriert werden.
|
||
|
||
### SSRF-Schutz
|
||
|
||
Alle CalDAV-URLs werden vor jedem HTTP-Request auf SSRF-Risiken geprüft. Standardmäßig sind folgende Adressbereiche gesperrt:
|
||
|
||
| Bereich | Grund |
|
||
|---------|-------|
|
||
| `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` | RFC 1918 (private Netze) |
|
||
| `127.0.0.0/8` | Loopback |
|
||
| `169.254.0.0/16` | Link-local / Cloud-Metadaten (AWS, GCP, Azure) |
|
||
| `::1/128`, `fc00::/7`, `fe80::/10` | IPv6 intern |
|
||
|
||
### Interne Nextcloud freischalten (CALDAV_ALLOWED_CIDRS)
|
||
|
||
Wenn die Nextcloud-Instanz **im internen Netz** betrieben wird (häufig bei On-Premises-Deployments), muss das entsprechende Netz explizit per `.env`-Variable freigeschaltet werden:
|
||
|
||
**Schritt 1 – `.env` auf dem Server bearbeiten:**
|
||
|
||
```bash
|
||
ssh root@192.168.1.137 "nano /opt/timemaster/backend/.env"
|
||
```
|
||
|
||
**Schritt 2 – Variable hinzufügen oder ergänzen:**
|
||
|
||
```bash
|
||
# Einzelner Host (empfohlen wenn möglich):
|
||
CALDAV_ALLOWED_CIDRS=192.168.1.50/32
|
||
|
||
# Ganzes Subnetz:
|
||
CALDAV_ALLOWED_CIDRS=192.168.1.0/24
|
||
|
||
# Mehrere Einträge kommasepariert:
|
||
CALDAV_ALLOWED_CIDRS=192.168.1.0/24,10.10.5.0/28
|
||
|
||
# Nextcloud auf Port 8080, nur dieser Host:
|
||
CALDAV_ALLOWED_CIDRS=10.0.1.20/32
|
||
```
|
||
|
||
**Schritt 3 – Service neu starten:**
|
||
|
||
```bash
|
||
ssh root@192.168.1.137 "systemctl restart timemaster"
|
||
```
|
||
|
||
**Schritt 4 – Verbindung testen** (in der TimeMaster-Oberfläche unter Einstellungen → CalDAV → „Verbindung testen").
|
||
|
||
> **Sicherheitshinweis:** So eng wie möglich einschränken – lieber `/32` (einzelner Host) als `/24` (ganzes Subnetz). Der Rest des internen Netzes bleibt weiterhin gesperrt.
|
||
|
||
### Nextcloud-Kalender-URL ermitteln
|
||
|
||
In Nextcloud die Kalender-URL findet man unter:
|
||
**Kalender-App → Kalender-Einstellungen (Zahnrad) → Primäre CalDAV-Adresse kopieren**
|
||
|
||
Typisches Format:
|
||
```
|
||
https://nextcloud.example.com/remote.php/dav/calendars/USERNAME/KALENDER-NAME/
|
||
```
|
||
|
||
Für den Firmenkalender empfiehlt sich ein dedizierter Nextcloud-User (z.B. `timemaster-sync`), damit keine persönlichen Zugangsdaten hinterlegt werden müssen.
|
||
|
||
Bei divergentem Datenbankzustand zwischen den Servern: Server 164 aus dem Dump von Server 137 wiederherstellen.
|