Files
timemaster/docs/deployment.md
T
2026-05-24 13:04:20 +02:00

14 KiB
Raw Blame History

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:

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:

# 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:

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:

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:

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:

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

cp /opt/timemaster/timemaster.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable timemaster
systemctl start timemaster

Umgebungsvariablen (.env)

Datei: /opt/timemaster/backend/.env

# === 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

[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:

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)

# 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:

apt-get install certbot python3-certbot-nginx
certbot --nginx -d yourdomain.com -d www.yourdomain.com

Nach nginx-Konfigurationsänderungen:

nginx -t && systemctl reload nginx

Deployment-Workflow (reguläre Updates)

Der gesamte Deployment-Prozess ist in update.sh automatisiert.

Vollständiges Deployment

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

./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)

# 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

# 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

curl https://yourdomain.com/health
# → { "status": "ok", "database": "ok", "redis": "ok" }

Service-Status

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

# 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

# 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

# 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

# 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:

ssh root@192.168.1.137 "nano /opt/timemaster/backend/.env"

Schritt 2 Variable hinzufügen oder ergänzen:

# 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:

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.