Production Deployment Guide¶
Diese Anleitung beschreibt das Deployment von LLARS in einer Produktionsumgebung.
Sicherheitshinweis
Produktionsdeployments erfordern zusätzliche Sicherheitsmaßnahmen. Diese Anleitung dient als Ausgangspunkt - passe sie an deine Anforderungen an.
Voraussetzungen¶
Hardware¶
| Komponente | Minimum | Empfohlen |
|---|---|---|
| CPU | 4 Cores | 8+ Cores |
| RAM | 16 GB | 32+ GB |
| Storage | 100 GB SSD | 500+ GB NVMe |
| Netzwerk | 100 Mbit/s | 1 Gbit/s |
Software¶
- Docker 24.0+
- Docker Compose 2.20+
- Git
- SSL-Zertifikat (Let's Encrypt oder eigenes)
1. Server vorbereiten¶
System-Updates¶
Docker ohne sudo¶
Firewall¶
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
2. LLARS installieren¶
Repository klonen¶
cd /var
sudo mkdir llars
sudo chown $USER:$USER llars
cd llars
git clone https://git.your-server.de/llars/llars.git .
Produktions-.env erstellen¶
Wichtige Einstellungen in .env:
# === PRODUCTION SETTINGS ===
FLASK_ENV=production
FLASK_DEBUG=False
# === SECRETS (ÄNDERN!) ===
SECRET_KEY=$(openssl rand -hex 32)
JWT_SECRET_KEY=$(openssl rand -hex 32)
# === DATABASE ===
MARIADB_USER=llars_prod
MARIADB_PASSWORD=$(openssl rand -hex 24)
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 24)
MARIADB_DATABASE=llars_production
# === AUTHENTIK ===
AUTHENTIK_SECRET_KEY=$(openssl rand -hex 32)
AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -hex 16)
AUTHENTIK_PUBLIC_URL=https://auth.your-domain.com
# === EXTERNAL ACCESS ===
NGINX_EXTERNAL_PORT=80
VITE_API_BASE_URL=https://llars.your-domain.com/api
# === LLM API ===
LITELLM_API_KEY=your-litellm-key
LITELLM_BASE_URL=https://your-litellm-proxy.com
# === PERFORMANCE ===
GUNICORN_WORKERS=4
GUNICORN_THREADS=4
3. SSL mit Reverse Proxy¶
Option A: Externer Reverse Proxy (empfohlen)¶
Wenn ein externer Nginx/Traefik/Caddy vorhanden ist:
Nginx-Konfiguration auf dem Proxy:
server {
listen 443 ssl http2;
server_name llars.your-domain.com;
ssl_certificate /etc/letsencrypt/live/llars.your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/llars.your-domain.com/privkey.pem;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://llars-server:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
# WebSocket Support
proxy_read_timeout 86400;
}
}
server {
listen 80;
server_name llars.your-domain.com;
return 301 https://$server_name$request_uri;
}
Option B: LLARS-internes SSL¶
Für eigenständiges Deployment ohne externen Proxy:
# Certbot installieren
sudo apt install certbot
# Zertifikat holen
sudo certbot certonly --standalone -d llars.your-domain.com
# Zertifikate nach LLARS kopieren
sudo cp /etc/letsencrypt/live/llars.your-domain.com/fullchain.pem /var/llars/ssl/
sudo cp /etc/letsencrypt/live/llars.your-domain.com/privkey.pem /var/llars/ssl/
4. Starten¶
Erster Start¶
Status prüfen¶
Health Check¶
5. Authentik einrichten¶
1. Authentik Admin-Panel öffnen¶
Login: akadmin / AUTHENTIK_BOOTSTRAP_PASSWORD aus .env
2. LLARS-Anwendung erstellen¶
- Admin → Applications → Create
- Name:
LLARS - Slug:
llars - Provider erstellen:
- Type: OAuth2/OpenID Provider
- Client ID:
llars-frontend - Client Secret: (generieren und in
.envspeichern) - Redirect URIs:
3. Backend-Provider erstellen¶
- Admin → Providers → Create
- Type: OAuth2/OpenID Provider
- Client ID:
llars-backend - Für Service-Account/API-Zugriff
6. Backup einrichten¶
Automatisches Datenbank-Backup¶
# /var/llars/scripts/backup.sh
#!/bin/bash
BACKUP_DIR=/var/llars/backups
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Database Backup
docker exec llars_db_service mysqldump -u root -p$MARIADB_ROOT_PASSWORD \
--all-databases > $BACKUP_DIR/db_$DATE.sql
# Compress
gzip $BACKUP_DIR/db_$DATE.sql
# ChromaDB Backup
tar -czf $BACKUP_DIR/chroma_$DATE.tar.gz /var/llars/app/storage/chroma
# Uploads Backup
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz /var/llars/app/storage/uploads
# Cleanup old backups (keep 30 days)
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete
echo "Backup completed: $DATE"
Cron-Job¶
# Tägliches Backup um 3:00 Uhr
echo "0 3 * * * /var/llars/scripts/backup.sh >> /var/log/llars-backup.log 2>&1" | crontab -
7. Monitoring¶
Docker-Stats¶
Log-Aggregation¶
# Alle Logs in eine Datei
docker compose logs -f > /var/log/llars/all.log 2>&1 &
# Nur Fehler
docker compose logs -f 2>&1 | grep -i error > /var/log/llars/errors.log &
Matomo Analytics¶
Bereits in LLARS integriert. Zugriff über:
8. Updates¶
Standard-Update¶
cd /var/llars
# Änderungen pullen
git pull origin main
# Container neu bauen
docker compose build
# Neustart
docker compose up -d
# Logs prüfen
docker compose logs -f --tail=100
Update mit Downtime¶
cd /var/llars
# Wartungsmodus aktivieren (optional)
docker exec llars_flask_service flask maintenance on
# Backup erstellen
./scripts/backup.sh
# Container stoppen
docker compose down
# Update
git pull origin main
# Neu starten
docker compose up -d --build
# Wartungsmodus deaktivieren
docker exec llars_flask_service flask maintenance off
9. Troubleshooting¶
502 Bad Gateway¶
# Prüfe ob Container laufen
docker compose ps
# Prüfe Nginx-Logs
docker logs llars_nginx_service
# Prüfe Flask-Logs
docker logs llars_flask_service
Datenbank-Verbindung fehlgeschlagen¶
# MariaDB-Status
docker logs llars_db_service
# Verbindungstest
docker exec llars_flask_service python -c "from db.db import db; print('OK')"
Authentik-Login schlägt fehl¶
- Prüfe
AUTHENTIK_PUBLIC_URLin.env - Prüfe Client-ID und Secret
- Prüfe Redirect-URIs in Authentik
ChromaDB-Fehler¶
# ChromaDB-Logs
docker logs llars_chromadb_service
# Vectorstore prüfen
ls -la /var/llars/app/storage/chroma/
10. Sicherheits-Checkliste¶
- Alle Standard-Passwörter in
.envgeändert - SSL/TLS aktiviert
- Firewall konfiguriert
- Regelmäßige Backups eingerichtet
- Log-Rotation aktiviert
- Authentik Admin-Passwort geändert
- Docker-Socket nur für Admins zugänglich
- Keine Debug-Logs in Produktion
- Rate-Limiting konfiguriert (Reverse Proxy)
- Security Headers gesetzt
11. WSGI Server (Gunicorn + Gevent)¶
LLARS verwendet in Production Gunicorn mit gevent-websocket für:
- Echte WebSocket-Unterstützung (kein Polling-Fallback)
- Bessere Docker DNS-Kompatibilität als eventlet
- Geringer Ressourcenverbrauch im Idle (~0.04% CPU, ~380 MB RAM)
Development vs Production Mode¶
Die Umgebungsvariable PROJECT_STATE steuert den Modus:
| Modus | Server | WebSocket | Auto-Reload |
|---|---|---|---|
development |
Flask Dev Server | Polling (threading) | ✓ |
production |
Gunicorn + gevent | Echte WebSockets | ✗ |
Gunicorn-Konfiguration¶
Die Konfiguration liegt in docker/flask/gunicorn.conf.py:
# Server
bind = '0.0.0.0:8081'
workers = 1 # Bei gevent: 1 Worker, Greenlets übernehmen Concurrency
# Worker-Klasse für echte WebSocket-Unterstützung
worker_class = 'geventwebsocket.gunicorn.workers.GeventWebSocketWorker'
# Timeouts für lange LLM-Requests
timeout = 300 # 5 Minuten
graceful_timeout = 30
# Memory-Leak-Prävention
max_requests = 1000
max_requests_jitter = 50
WSGI Entry Points¶
| Datei | Verwendung |
|---|---|
wsgi_gevent.py |
Production (Gunicorn + gevent) |
wsgi.py |
Eventlet (nicht empfohlen für Docker) |
Wichtig: gevent.monkey.patch_all() MUSS vor allen anderen Imports aufgerufen werden!
Async Mode¶
Flask-SocketIO erkennt den async_mode automatisch via Umgebungsvariable:
# Wird in start_flask.sh gesetzt
export SOCKETIO_ASYNC_MODE="gevent" # Production
export SOCKETIO_ASYNC_MODE="threading" # Development
12. Performance-Tuning¶
Gunicorn-Worker¶
# In .env (für sync workers, nicht bei gevent)
GUNICORN_WORKERS=4 # CPU-Cores * 2 + 1
GUNICORN_THREADS=4 # Pro Worker
GUNICORN_TIMEOUT=120 # Für lange Requests
MariaDB¶
# my.cnf Anpassungen
innodb_buffer_pool_size = 4G # 50-70% des RAM
innodb_log_file_size = 1G
max_connections = 200
ChromaDB¶
13. Load Testing¶
LLARS enthält ein integriertes Load-Test-Skript unter scripts/load_test.py.
Quick Test¶
# Lokal
python scripts/load_test.py --quick --host localhost --port 8081
# Im Docker-Container
docker exec llars_flask_service python3 /app/scripts/load_test.py --quick
Heavy Load Test¶
# 100 gleichzeitige User, 20 Requests pro User, 50 WebSocket-Verbindungen
python scripts/load_test.py \
--users 100 \
--requests 20 \
--ws-connections 50 \
--duration 60
Test-Arten¶
| Parameter | Beschreibung |
|---|---|
--test http |
Nur HTTP API-Endpunkte |
--test websocket |
Nur WebSocket-Verbindungen |
--test sustained |
Dauerlast über Zeit |
--test all |
Alle Tests (Standard) |
Benchmark-Ergebnisse (Stand: Januar 2026)¶
Server: Production mit Gunicorn + gevent-websocket
| Metrik | Wert |
|---|---|
| HTTP Response Time (avg) | 8-80 ms |
| HTTP Response Time (p95) | 164 ms |
| HTTP Response Time (p99) | 349 ms |
| Throughput | ~100 req/s |
| WebSocket Connect Time | 135-272 ms |
| WebSocket Success Rate | 100% |
Ressourcen nach Heavy Load (9.600 Requests):
| Container | CPU | RAM |
|---|---|---|
| Flask | 0.04% | 380 MB |
| MariaDB | 0.02% | 112 MB |
| nginx | 0.00% | 8 MB |
| Redis | 0.63% | 7 MB |
Rate-Limiting¶
Flask-Limiter schützt die API automatisch:
- Standard: 1000 Requests/Stunde pro Endpoint
- Status-Code:
429 Too Many Requests - Log:
ratelimit exceeded at endpoint: ...
14. Blue-Green Deployment¶
Seit März 2026 nutzt LLARS Blue-Green Deployment für near-zero-downtime Updates.
Konzept¶
┌─────────┐
│ NGINX │
└────┬────┘
│
┌────────────┼────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ BLUE (alt) │ │ GREEN (neu) │
│ Flask+Vue+YJS│ │ Flask+Vue+YJS│
└───────────────┘ └───────────────┘
- Aktive Farbe bedient Production-Traffic
- Inaktive Farbe wird für das nächste Deployment gebaut
- Umschaltung erfolgt durch Nginx-Config-Update (~2 Sekunden Downtime)
Befehle¶
# Status anzeigen
bash scripts/ci/manual_bluegreen_deploy.sh status
# Inaktive Farbe bauen + auf Staging bereitstellen
bash scripts/ci/manual_bluegreen_deploy.sh prepare
# Smoke-Tests gegen Staging ausführen
bash scripts/ci/manual_bluegreen_deploy.sh test
# Auf Production umschalten
bash scripts/ci/manual_bluegreen_deploy.sh switch
Automatisches Deployment (CI/CD)¶
Development-Branch (dev) deployed automatisch bei jedem Push auf den Dev-Server (llars-dev, 141.75.150.86). Production-Branch (main) deployed nachts Mo-Fr 02:00 CET via GitLab Pipeline Schedule.
Production Nightly-Flow:
Bei fehlgeschlagenen Smoke-Tests: automatischer Rollback.
Pipeline-Steuerung¶
| Trigger | Staging | Production | Beschreibung |
|---|---|---|---|
| Normaler Push | ✗ | ✗ | Nur Lint + Tests |
[dryrun] in Commit-Message |
✓ | ✗ | Voller Staging-Flow, kein Prod-Deploy |
DRY_RUN=true (CI Variable) |
✓ | ✗ | Gleich wie [dryrun] |
FORCE_DEPLOY=true (CI Variable) |
✓ | ✓ | Sofort volle Pipeline |
| Nightly Schedule | ✓ | ✓ | Mo-Fr 02:00 CET |
Dry-Run prüft ob der Nightly-Lauf durchlaufen wird, ohne Production zu berühren:
Rollback¶
# Sofort-Rollback (~2s, Nginx zurückschalten)
bash scripts/ci/rollback_bluegreen.sh
# Fallback-Rollback (DB-Restore + Rebuild, 5-10 Min)
bash scripts/ci/rollback_production.sh
Wichtige Dateien¶
| Datei | Beschreibung |
|---|---|
docker-compose.prod-bluegreen.yml |
Compose-Override für Blue/Green |
docker/nginx/active_upstream.conf |
Aktive Farbe (Production) |
docker/nginx/active_upstream_staging.conf |
Staging-Upstream |
.deploy/active_color |
Aktuell aktive Farbe |
scripts/ci/deploy_bluegreen.sh |
Deploy-Script |
scripts/ci/rollback_bluegreen.sh |
Rollback-Script |
15. Troubleshooting Production¶
Eventlet DNS-Probleme¶
Problem: Mit eventlet in Docker:
Lösung: Gevent statt eventlet verwenden (bereits Standard):
WebSocket-Fallback auf Polling¶
Problem: Browser nutzt Polling statt WebSocket.
Prüfen:
# Im Container
docker exec llars_flask_service python3 -c "
import socketio
sio = socketio.Client()
sio.connect('http://localhost:8081', transports=['websocket'])
print('Transport:', sio.transport()) # Sollte 'websocket' sein
sio.disconnect()
"
Lösung: Sicherstellen dass PROJECT_STATE=production und Gunicorn mit gevent läuft.
Container startet nicht nach Update¶
Problem: Permission denied bei start_flask.sh.
Lösung: