# 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= FRONTEND_URL=https://yourdomain.com ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com # === Datenbank === DATABASE_URL=postgresql+asyncpg://timemaster:@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_ 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= ``` 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 " 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.