# Infrastructure Guide > **Version:** 2.0 > **Date:** 2026-08-20 > **Applies to:** System administrators and DevOps engineers --- ## 1. Docker-Compose Stack LeoCRM läuft als Docker-Compose-Stack mit 4 Services: | Service | Image | Beschreibung | |---------|-------|-------------| | **postgres** | `pgvector/pgvector:pg16` | PostgreSQL 16 mit pgvector Extension für Vector Search | | **redis** | `redis:7-alpine` | Redis 7 für Sessions, Caching, Pub/Sub, ARQ Queue | | **crm_app** | Multi-Stage Build | FastAPI Backend (uvicorn), API + WebSocket | | **crm_worker** | Multi-Stage Build | ARQ Background Worker (Cron-Jobs, Queue Processing) | ### docker-compose.yaml Übersicht ```yaml services: postgres: image: pgvector/pgvector:pg16 volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U crm_user -d crm_db"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 crm_app: build: . command: ["./prestart.sh"] depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } healthcheck: test: ["CMD-SHELL", "bash /app/healthcheck.sh"] interval: 30s timeout: 10s retries: 3 start_period: 15s crm_worker: build: . command: ["./worker.sh"] depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } ``` ### Container-Entrypoints | Datei | Service | Funktion | |------|---------|----------| | `prestart.sh` | crm_app | Alembic-Migrationen → DB-Role-Passwörter → Plugin-Schema-Sync → Admin-Seed → uvicorn Start | | `worker.sh` | crm_worker | ARQ Worker Start mit Cron-Jobs | | `healthcheck.sh` | crm_app | HTTP `/api/v1/health` oder Redis-Ping | ### Volumes | Volume | Service | Beschreibung | |--------|---------|-------------| | `postgres_data` | postgres | PostgreSQL Daten (persistent) | | `redis_data` | redis | Redis Snapshot (persistent) | | `app_storage` | crm_app | File Storage (DMS, Uploads) | --- ## 2. Coolify Deployment LeoCRM ist über Coolify auf einem Hetzner VPS deployiert. ### Server-Info | Eigenschaft | Wert | |-------------|------| | **Host** | 46.225.91.159 | | **Hostname** | coolify-01 | | **Provider** | Hetzner VPS | | **Coolify URL** | https://server.media-on.de | | **App URL** | https://crm.media-on.de | | **App UUID** | xf7smknlger3hvkrsb910tui | | **Container-Name** | Ändert sich bei jedem Coolify-Deploy (Suffix) | ### Deploy-Methoden #### Frontend-Only Deploy (~20s) ```bash bash /a0/usr/projects/leocrm/scripts/fast-deploy.sh frontend ``` - Baut Frontend lokal, kopiert `dist/` direkt in den laufenden Container - Kein Coolify-Rebuild, kein Docker-Image-Neubau - Container wird nicht neu gestartet #### Full Deploy (~2min, für Backend-Änderungen) ```bash bash /a0/usr/projects/leocrm/scripts/fast-deploy.sh full ``` - Triggert Coolify-Rebuild über `deploy.py` - Für Python-Code, Requirements, Migrations ### Server-Container Auf dem Hetzner VPS laufen ~25 Docker-Container (Coolify + Services): - Coolify Proxy (Traefik) - Coolify Dashboard - Coolify Database - LeoCRM Stack (postgres, redis, crm_app, crm_worker) - Weitere Coolify-managed Services Container-Übersicht auf dem Server: ```bash ssh -i ~/.ssh/coolify-01-root root@46.225.91.159 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"' ``` --- ## 3. ARQ Worker & Cron-Jobs Der `crm_worker` Service läuft ARQ (Async Redis Queue) für Background-Jobs. ### Registrierte Cron-Jobs | Job | Schedule | Beschreibung | |-----|----------|-------------| | `auto_backup_job` | Täglich 03:00 | Automatisches Backup (ruft `scripts/backup.py` auf) | | `audit_retention_cleanup` | Täglich 04:00 | Audit-Logs älter als 365 Tage archivieren/löschen | | `cleanup_expired_trash` | Täglich 05:00 | Soft-deleted Entitäten älter als 90 Tage endgültig löschen | | `outbox_cleanup` | Stündlich | Outbox-Einträge älter als 30 Tage löschen | | `cleanup_expired_sessions` | Stündlich | Abgelaufene Sessions aus Redis löschen | ### ARQ Worker Konfiguration ```python # app/core/worker.py class WorkerSettings: functions = [...] cron_jobs = [ cron(auto_backup_job, hour=3, minute=0), cron(audit_retention_cleanup, hour=4, minute=0), cron(cleanup_expired_trash, hour=5, minute=0), cron(outbox_cleanup, hour={0,6,12,18}, minute=0), cron(cleanup_expired_sessions, hour={0,6,12,18}, minute=0), ] max_jobs = 10 job_timeout = 300 queue_name = "arq:queue" ``` ### Worker-Stats abfragen ```bash # Queue-Länge redis-cli ZCARD arq:queue # Aktive Worker redis-cli KEYS "arq:heartbeat:*" # Via API (Admin) curl -b "leocrm_session=" https://crm.media-on.de/api/v1/system/dashboard | jq '.worker' ``` --- ## 4. PgBouncer Setup PgBouncer ist ein leichter Connection-Pooler für PostgreSQL. Er reduziert den Overhead neuer Datenbankverbindungen durch Wiederverwendung bestehender Verbindungen. ### Warum PgBouncer? - **Connection pooling** — Reduziert PostgreSQL Connection-Overhead - **Resource efficiency** — Verwaltet tausende Client-Verbindungen mit minimalen Ressourcen - **Transaction pooling** — Optimal für stateless Anwendungen wie FastAPI - **Session pooling** — Für stateful Verbindungen - **Statement pooling** — Für spezifische Use-Cases ### Installation ```bash # Debian/Ubuntu apt-get update && apt-get install -y pgbouncer # Verify installation pgbouncer --version ``` ### Konfiguration Create `/etc/pgbouncer/pgbouncer.ini`: ```ini [databases] leocrm = host=localhost port=5432 dbname=leocrm leocrm_test = host=localhost port=5432 dbname=leocrm_test [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 unix_socket_dir = /var/run/pgbouncer # Authentication auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt # Pool settings pool_mode = transaction default_pool_size = 25 max_client_conn = 200 max_db_connections = 50 # Timeouts server_idle_timeout = 600 server_lifetime = 3600 client_idle_timeout = 1800 query_timeout = 30 # Logging log_connections = 1 log_disconnections = 1 log_pooler_errors = 1 stats_period = 60 # Security listen_backlog = 128 ``` ### User List Create `/etc/pgbouncer/userlist.txt`: ``` "leocrm" "md5" "postgres" "md5" ``` Generate the md5 hash: ```bash # Format: md5 + md5(password + username) echo -n "md5" && echo -n "your_passwordleocrm" | md5sum | cut -d' ' -f1 ``` ### Running PgBouncer ```bash # Start PgBouncer pgbouncer -d /etc/pgbouncer/pgbouncer.ini # Check status pgbouncer -d /etc/pgbouncer/pgbouncer.ini -R # Reload configuration kill -HUP $(cat /var/run/pgbouncer/pgbouncer.pid) # Stop PgBouncer kill -INT $(cat /var/run/pgbouncer/pgbouncer.pid) ``` ### Docker Compose Integration Add to `docker-compose.yml`: ```yaml services: pgbouncer: image: bitnami/pgbouncer:latest container_name: leocrm-pgbouncer ports: - "6432:6432" environment: - POSTGRESQL_HOST=crm-postgres - POSTGRESQL_PORT=5432 - POSTGRESQL_USERNAME=leocrm - POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD} - POSTGRESQL_DATABASE=crm_db - PGBOUNCER_POOL_MODE=transaction - PGBOUNCER_DEFAULT_POOL_SIZE=25 - PGBOUNCER_MAX_CLIENT_CONN=200 depends_on: - crm-postgres restart: unless-stopped ``` ### Application Configuration Update the database URL to use PgBouncer: ```python # Before (direct connection) DATABASE_URL = "postgresql+asyncpg://leocrm:password@crm-postgres:5432/crm_db" # After (via PgBouncer) DATABASE_URL = "postgresql+asyncpg://leocrm:password@leocrm-pgbouncer:6432/crm_db" ``` ### Monitoring ```bash # Show pool statistics echo "SHOW STATS;" | psql -h localhost -p 6432 -U leocrm -d pgbouncer # Show active pools echo "SHOW POOLS;" | psql -h localhost -p 6432 -U leocrm -d pgbouncer # Show clients echo "SHOW CLIENTS;" | psql -h localhost -p 6432 -U leocrm -d pgbouncer # Show servers echo "SHOW SERVERS;" | psql -h localhost -p 6432 -U leocrm -d pgbouncer ``` ### Troubleshooting | Issue | Cause | Solution | |-------|-------|----------| | Connection refused | PgBouncer not running | Check `pgbouncer -d` status | | Auth failed | Wrong password in userlist | Regenerate md5 hash | | Pool exhausted | Too many connections | Increase `default_pool_size` | | Slow queries | Query timeout | Check `query_timeout` setting | | Connection timeout | PostgreSQL overload | Check PostgreSQL connections | --- ## 5. Audit Log Partitioning Die `audit_log` Tabelle kann sehr groß werden. PostgreSQL Table Partitioning hilft durch Aufteilung in kleinere, verwaltbare Stücke. ### Warum Partitioning? - **Schnellere Queries** — Queries scannen nur relevante Partitionen - **Einfachere Wartung** — Alte Partitionen droppen statt DELETE - **Besseres Vacuum** — Jede Partition wird unabhängig gevacuumt - **Bessere Performance** — Kleinere Indexes pro Partition ### Partitioning-Strategie Wir verwenden **monatliches Range-Partitioning** auf der `created_at` Spalte: ```sql -- Jede Partition deckt einen Monat ab -- Partitions-Name: audit_log_YYYY_MM -- Beispiel: audit_log_2026_01, audit_log_2026_02, ... ``` ### Creating the Partitioned Table ```sql -- Create the partitioned table CREATE TABLE audit_log_partitioned ( id UUID NOT NULL DEFAULT gen_random_uuid(), tenant_id UUID, user_id UUID, action VARCHAR(100) NOT NULL, entity_type VARCHAR(50), entity_id UUID, changes JSONB, ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); -- Create monthly partitions CREATE TABLE audit_log_2026_01 PARTITION OF audit_log_partitioned FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); CREATE TABLE audit_log_2026_02 PARTITION OF audit_log_partitioned FOR VALUES FROM ('2026-02-01') TO ('2026-03-01'); CREATE TABLE audit_log_2026_03 PARTITION OF audit_log_partitioned FOR VALUES FROM ('2026-03-01') TO ('2026-04-01'); -- Add indexes on each partition CREATE INDEX idx_audit_log_2026_01_tenant ON audit_log_2026_01 (tenant_id); CREATE INDEX idx_audit_log_2026_01_action ON audit_log_2026_01 (action); CREATE INDEX idx_audit_log_2026_01_entity ON audit_log_2026_01 (entity_type, entity_id); CREATE INDEX idx_audit_log_2026_01_created ON audit_log_2026_01 (created_at DESC); ``` ### Automating Partition Creation Use the `setup_audit_partitioning.sql` script to automate partition management: ```bash # Run the setup script psql -h localhost -U leocrm -d crm_db -f scripts/setup_audit_partitioning.sql ``` ### Cron Job for Partition Maintenance ```bash # Run on the 1st of each month at 2 AM 0 2 1 * * /usr/bin/psql -h localhost -U leocrm -d crm_db -c "SELECT create_monthly_audit_partition();" ``` ### Dropping Old Partitions ```sql -- Drop partitions older than retention period DROP TABLE IF EXISTS audit_log_2025_01; DROP TABLE IF EXISTS audit_log_2025_02; ``` ### Monitoring Partition Health ```sql -- Check partition sizes SELECT relname AS partition_name, pg_size_pretty(pg_total_relation_size(relid)) AS total_size FROM pg_catalog.pg_statio_user_tables WHERE relname LIKE 'audit_log_%' ORDER BY relname; -- Check row counts per partition SELECT relname AS partition_name, n_live_tup AS row_count FROM pg_catalog.pg_stat_user_tables WHERE relname LIKE 'audit_log_%' ORDER BY relname; ``` --- ## 6. Backup and Recovery ### Database Backup ```bash # Full backup pg_dump -h localhost -U leocrm -d crm_db -F c -f /backups/crm_db_$(date +%Y%m%d).dump # Backup with compression pg_dump -h localhost -U leocrm -d crm_db -F c -Z 9 -f /backups/crm_db_$(date +%Y%m%d).dump.gz ``` ### Automated Backup LeoCRM hat einen automatisierten Backup via ARQ Cron-Job: - **Job:** `auto_backup_job` (täglich 03:00 Uhr) - **Script:** `scripts/backup.py` (pg_dump + files) - **Konfiguration:** Settings → Backup (backup_enabled, backup_interval, backup_retention_days, backup_destination) - **API:** `POST /api/v1/system-settings/backup-now` (manueller Trigger) - **History:** `GET /api/v1/system-settings/backup-history` (letzte 10 Backups) - **Bei Fehler:** System-Message an Communication-System ### Database Restore ```bash # Restore full backup pg_restore -h localhost -U leocrm -d crm_db -c /backups/crm_db_20260701.dump # Restore with parallel workers (faster) pg_restore -h localhost -U leocrm -d crm_db -j 4 -c /backups/crm_db_20260701.dump ``` See `scripts/restore.py` for the automated restore solution. --- ## 7. Monitoring and Alerts ### Key Metrics | Metric | Target | Alert Threshold | |--------|--------|----------------| | Database connections | < 50 | > 80% of max | | Query response time | < 100ms | > 500ms | | Cache hit ratio | > 95% | < 90% | | Partition size | < 10GB | > 50GB | | PgBouncer pool usage | < 80% | > 90% | | Worker queue length | < 100 | > 500 | | Error rate | < 1% | > 5% | ### Health Checks ```bash # Check PgBouncer status echo "SHOW STATS;" | psql -h localhost -p 6432 -U leocrm -d pgbouncer | grep -E "total_|avg_" # Check partition health psql -h localhost -U leocrm -d crm_db -c "SELECT count(*) FROM audit_log WHERE created_at < NOW() - INTERVAL '3 months';" # Check database size psql -h localhost -U leocrm -d crm_db -c "SELECT pg_size_pretty(pg_database_size('crm_db'));" # Via API (Admin) curl -b "leocrm_session=" https://crm.media-on.de/api/v1/system/dashboard | jq . ``` --- ## 8. Production Resource Recommendations Die `docker-compose.yaml` hat Development-Defaults. Für Produktion mit 50+ Usern müssen die Limits erhöht werden. | Service | Development | Production (50+ User) | Begründung | |---|---|---|---| | **PostgreSQL RAM** | 512m | 1-2GB | pgvector HNSW + FTS + JSONB Snapshots + Outbox + AuditLog | | **PostgreSQL CPU** | 1.0 | 2.0 | Vector Search + FTS + normale CRM-Queries | | **PostgreSQL Disk** | Named Volume | 50-100GB | Embeddings (768 dim × 100k = ~300MB), JSONB Snapshots, Outbox | | **Redis RAM** | 128m | 256-512m | WS Pub/Sub + Caching + Sessions + ARQ + Rate-Limiting | | **Redis CPU** | 0.5 | 1.0 | Pub/Sub + Cache + Queue | | **App (FastAPI) RAM** | Nicht limitiert | 512m-1GB | WebSocket Connections + Async Tasks | | **App CPU** | Nicht limitiert | 1-2 CPUs | API + WS + LLM-Streaming | | **Worker (ARQ) RAM** | Nicht limitiert | 256-512m | Background Jobs (Indexierung, Agent-Runs, Extraction) | | **Worker CPU** | Nicht limitiert | 1-2 CPUs | LLM-Calls + Embedding + Text-Extraction | ### Skalierung bei Bedarf - **Read-Replicas:** Bei hohem Lese-Aufkommen (Search, FTS, Vector) können Read-Replicas für PostgreSQL eingerichtet werden. - **Mehr Worker:** Bei hohem Background-Job-Aufkommen können zusätzliche ARQ-Worker-Container gestartet werden. - **pgvector auslagern:** Bei sehr großen Datasets (>1M Embeddings) kann pgvector auf einen separaten PostgreSQL-Node ausgelagert werden. - **Redis Cluster:** Bei sehr hohem Cache-/Pub/Sub-Aufkommen kann Redis Cluster eingesetzt werden.