# LeoCRM — Vollständige Installationsanleitung **Stand:** 2026-08-01 **Commit:** be20a85 **Alembic-Head:** 0090 Diese Anleitung beschreibt die komplette Installation von LeoCRM von Grund auf. Keine manuellen Nacharbeiten erforderlich. Alle Schritte sind reproduzierbar. --- ## Voraussetzungen - Coolify v4 (oder Docker + Docker Compose) - PostgreSQL 16 mit pgvector-Extension - Redis 7 - Git-Zugang zum Forgejo-Repo - Domain mit DNS-Eintrag --- ## 1. Repository klonen ```bash git clone https://forgejo.media-on.de/Leopoldadmin/leocrm.git cd leocrm git checkout main ``` --- ## 2. Docker-Image bauen ```bash docker build -t leocrm:latest . ``` **Dockerfile-Hinweise:** - Verwendet `npm ci --legacy-peer-deps` (vite 8 peer dependency conflict) - Frontend wird in Multi-Stage-Build gebaut - Runtime-Image enthält: Python 3.12, Node.js, prestart.sh, worker.sh, healthcheck.sh --- ## 3. Datenbankrollen LeoCRM verwendet 5 separate Datenbankrollen mit unterschiedlichen Berechtigungen. Diese Rollen werden **automatisch** durch Migration 0085 erstellt. ### Rollen-Übersicht | Rolle | Superuser | BYPASSRLS | Login | Verwendung | |-------|----------|-----------|-------|-----------| | crm_user | Ja | Ja | Ja | PostgreSQL-Container-Admin (POSTGRES_USER) | | crm_migration | Nein | Ja | Ja | Alembic-Migrationen, Plugin-Migrationen (DDL) | | crm_auth | Nein | Nein | Ja | Login, Authentifizierung, Password-Reset | | crm_api | Nein | Nein | Ja | Normale API-Abfragen (SELECT, INSERT, UPDATE, DELETE) | | crm_worker | Nein | Nein | Ja | ARQ-Worker, Outbox-Processing, Cron-Jobs | ### Bootstrap-Reihenfolge ``` 1. PostgreSQL-Container startet → crm_user wird erstellt (POSTGRES_USER, SUPERUSER) 2. prestart.sh läuft im API-Container → alembic upgrade head (als crm_user über MIGRATION_DATABASE_URL) → Migration 0085 erstellt crm_migration, crm_auth, crm_api, crm_worker → Migration 0085 vergibt Grants und erstellt RLS-Policies → prestart.sh setzt Passwörter für alle Rollen 3. API startet (uvicorn) → Verwendet DATABASE_URL (crm_api) für normale Abfragen → Verwendet AUTH_DATABASE_URL (crm_auth) für Login → Plugin-Migrationen über get_migration_engine() (crm_migration) 4. Worker startet → Verwendet WORKER_DATABASE_URL (crm_worker) für Jobs → Plugin-Migrationen über get_migration_engine() (crm_migration) → Event-Handler für aktive Plugins registriert ``` ### WICHTIG: MIGRATION_DATABASE_URL Der erste Alembic-Lauf auf einer leeren Datenbank MUSS als `crm_user` ausgeführt werden, weil `crm_migration` erst durch Migration 0085 erstellt wird. ``` MIGRATION_DATABASE_URL=postgresql+asyncpg://crm_user:@db:5432/crm_db ``` Nach Migration 0085 kann MIGRATION_DATABASE_URL auf `crm_migration` umgestellt werden, aber für den Bootstrap-Prozess ist `crm_user` erforderlich. --- ## 4. Environment-Variablen ### API-Container | Variable | Wert | Beschreibung | |----------|------|-------------| | DATABASE_URL | postgresql+asyncpg://crm_api:PW@db:5432/crm_db | API-Abfragen (crm_api) | | AUTH_DATABASE_URL | postgresql+asyncpg://crm_auth:PW@db:5432/crm_db | Login/Auth (crm_auth) | | WORKER_DATABASE_URL | postgresql+asyncpg://crm_worker:PW@db:5432/crm_db | Worker-Jobs (crm_worker) | | MIGRATION_DATABASE_URL | postgresql+asyncpg://crm_user:PW@db:5432/crm_db | Migrationen (crm_user für Bootstrap) | | REDIS_URL | redis://default:PW@redis:6379/0 | Redis-Verbindung | | SECRET_KEY | | Session-Verschlüsselung | | ENVIRONMENT | production | Umgebung | | STORAGE_PATH | /data/storage | Datei-Storage | | FRONTEND_URL | https://crm.example.com | Frontend-URL | | CORS_ORIGINS | https://crm.example.com | CORS-Konfiguration | | SESSION_COOKIE_SECURE | true | HTTPS-Cookies | | LOG_LEVEL | INFO | Logging-Level | ### Worker-Container | Variable | Wert | Beschreibung | |----------|------|-------------| | DATABASE_URL | postgresql+asyncpg://crm_worker:PW@db:5432/crm_db | Worker-DB (crm_worker) | | WORKER_DATABASE_URL | postgresql+asyncpg://crm_worker:PW@db:5432/crm_db | Worker-DB (crm_worker) | | MIGRATION_DATABASE_URL | postgresql+asyncpg://crm_user:PW@db:5432/crm_db | Plugin-Migrationen (crm_user) | | REDIS_URL | redis://default:PW@redis:6379/0 | Redis-Verbindung | | SECRET_KEY | | Session-Verschlüsselung | | ENVIRONMENT | production | Umgebung | | STORAGE_PATH | /data/storage | Datei-Storage | ### DB-Container | Variable | Wert | |----------|------| | POSTGRES_USER | crm_user | | POSTGRES_PASSWORD | | | POSTGRES_DB | crm_db | ### WICHTIG: Alle DB-Passwörter sind identisch Migration 0085 erstellt die Rollen `crm_api`, `crm_auth`, `crm_worker`, `crm_migration` mit demselben Passwort das in `MIGRATION_DATABASE_URL` für `crm_user` konfiguriert ist. `prestart.sh` setzt anschließend die Passwörter für alle Rollen aus der `MIGRATION_DATABASE_URL`. Daher müssen alle `DATABASE_URL`, `AUTH_DATABASE_URL`, `WORKER_DATABASE_URL` dasselbe Passwort verwenden wie `MIGRATION_DATABASE_URL`. --- ## 5. Docker Compose ### Vollständige docker-compose.yml ```yaml version: '3.8' services: db: image: pgvector/pgvector:pg16 restart: unless-stopped environment: POSTGRES_USER: crm_user POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: crm_db volumes: - db-data:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U crm_user -d crm_db'] interval: 5s timeout: 10s retries: 20 redis: image: redis:7-alpine restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis-data:/data api: image: leocrm:latest restart: unless-stopped expose: - '8000' environment: DATABASE_URL: postgresql+asyncpg://crm_api:${DB_PASSWORD}@db:5432/crm_db AUTH_DATABASE_URL: postgresql+asyncpg://crm_auth:${DB_PASSWORD}@db:5432/crm_db WORKER_DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@db:5432/crm_db MIGRATION_DATABASE_URL: postgresql+asyncpg://crm_user:${DB_PASSWORD}@db:5432/crm_db REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0 SECRET_KEY: ${SECRET_KEY} ENVIRONMENT: production STORAGE_PATH: /data/storage FRONTEND_URL: https://crm.example.com CORS_ORIGINS: https://crm.example.com SESSION_COOKIE_SECURE: 'true' LOG_LEVEL: INFO volumes: - api-storage:/data/storage depends_on: db: condition: service_healthy redis: condition: service_started healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:8000/api/v1/health'] interval: 30s timeout: 10s retries: 3 start_period: 180s worker: image: leocrm:latest restart: unless-stopped entrypoint: /app/worker.sh environment: DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@db:5432/crm_db WORKER_DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@db:5432/crm_db MIGRATION_DATABASE_URL: postgresql+asyncpg://crm_user:${DB_PASSWORD}@db:5432/crm_db REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0 SECRET_KEY: ${SECRET_KEY} ENVIRONMENT: production STORAGE_PATH: /data/storage depends_on: db: condition: service_healthy redis: condition: service_started volumes: db-data: redis-data: api-storage: ``` ### .env Datei ```env DB_PASSWORD=YourSecurePassword2026 REDIS_PASSWORD=YourRedisPassword2026 SECRET_KEY=your-secret-key-with-at-least-32-characters!! ``` --- ## 6. Coolify-Setup ### 6.1 Neue Anwendung erstellen (automatisiert) LeoCRM wird als einzelner docker-compose Stack in Coolify deployt. Alle 4 Container (postgres, redis, crm_app, crm_worker) laufen in einer Coolify Application mit `build_pack=dockercompose`. ```bash # Umgebungsvariablen setzen export COOLIFY_API_TOKEN="dein-token" export APP_DOMAIN="https://crm.example.com" export APP_NAME="leocrm" export DB_PASSWORD="YourSecurePassword2026" export REDIS_PASSWORD="YourRedisPassword2026" export SECRET_KEY="your-secret-key-with-at-least-32-characters!!" export COOLIFY_PROJECT_UUID="" export COOLIFY_SERVER_UUID="" export COOLIFY_PRIVATE_KEY_UUID="" # Initial deployment (2-Phase: ohne Domain, dann mit Domain + Redeploy) python scripts/deploy.py --initial ``` Das Script macht automatisch: 1. Coolify Application via `private-deploy-key` erstellen 2. Build Pack auf `dockercompose` setzen (liest docker-compose.yaml aus Git) 3. Environment-Variablen setzen (Secrets, Domain, Admin-Credentials) 4. Erster Deploy (ohne Domain — Coolify muss docker-compose.yaml lesen) 5. `docker_compose_domains` setzen + Redeploy (mit Traefik-Labels) 6. Verifikation (HTTP, Login, Alembic, RLS) ### 6.2 Redeploy (bestehende Anwendung) ```bash export COOLIFY_API_TOKEN="dein-token" export APP_DOMAIN="https://crm.example.com" python scripts/deploy.py ``` ### 6.3 Wichtige Hinweise - **docker-compose.yaml** (nicht `.yml`) — Coolify sucht nach `.yaml` - **Service-Namen mit Unterstrichen**: `crm_app`, `crm_worker` (nicht mit Bindestrich) - **Domain ohne `:443`** in `docker_compose_domains` — `:443` führt zu leeren Traefik-Labels - **Kein `connect_to_docker_network`** — Coolify kümmert sich selbst um das Netzwerk im Stack - **DB-Image**: MUSS `pgvector/pgvector:pg16` sein (nicht `postgres:16-alpine`) ### 6.4 Environment-Variablen in Coolify Das `--initial` Script setzt automatisch alle benötigten ENV-Variablen in Coolify. Die `docker-compose.yaml` verwendet `${VARIABLE}` Syntax — Coolify substituiert aus diesen ENV-Variablen. Siehe auch: `DEPLOY.md` und `COOLIFY_SETUP.md` für Details. --- ## 7. Startup-Ablauf (prestart.sh) `prestart.sh` wird beim API-Container-Start ausgeführt: ``` 1. Warten auf PostgreSQL (pg_isready) 2. alembic upgrade head (als crm_user über MIGRATION_DATABASE_URL) → Migrationen 0001-0090 werden ausgeführt → Migration 0085 erstellt DB-Rollen, RLS-Policies, Grants 3. Passwörter für alle Rollen setzen → Extrahiert Passwort aus MIGRATION_DATABASE_URL → SET PASSWORD für crm_api, crm_auth, crm_worker, crm_migration 4. Uvicorn starten ``` --- ## 8. Admin-User anlegen Nach erfolgreichem Start: ```bash docker exec api-container python3 scripts/seed_admin.py ``` Erstellt: - Tenant: "Default Org" (slug: default) - Admin-Role mit permissions={"*:*": True} - User: admin@media-on.de / Admin123! - UserTenant-Link mit Admin-Role **Passwort ändern:** `ADMIN_PASSWORD` Environment-Variable setzen vor Ausführung. --- ## 9. Verifikation ### 9.1 Health-Check ```bash curl https://crm.example.com/api/v1/health # Erwartet: {"status":"healthy",...} ``` ### 9.2 Login-Test ```bash curl -X POST https://crm.example.com/api/v1/auth/login \ -H "Content-Type: application/json" \ -H "Origin: https://crm.example.com" \ -d '{"email":"admin@media-on.de","password":"Admin123!"}' # Erwartet: 200 OK mit user_id, csrf_token, tenant_id ``` ### 9.3 Alembic-Head prüfen ```bash docker exec api-container python3 -m alembic current # Erwartet: 0090 (head) ``` ### 9.4 RLS prüfen ```sql -- Als crm_api ohne Tenant-Kontext: 0 rows SET ROLE crm_api; SELECT count(*) FROM contacts; -- Erwartet: 0 RESET ROLE; -- Als crm_api mit Tenant-Kontext: Tenant-Daten SET ROLE crm_api; SET app.current_tenant_id = ''; SELECT count(*) FROM contacts; -- Erwartet: > 0 RESET ROLE; ``` ### 9.5 DDL durch crm_api blockiert ```sql SET ROLE crm_api; CREATE TABLE test_block (id int); -- Erwartet: permission denied RESET ROLE; ``` --- ## 10. SMTP-Konfiguration (optional) Für Passwort-Reset-Emails: | Variable | Wert | |----------|------| | SMTP_HOST | mail.example.com | | SMTP_PORT | 465 | | SMTP_USER | noreply@example.com | | SMTP_PASSWORD | | | SMTP_FROM_EMAIL | noreply@example.com | | SMTP_USE_TLS | true | **Wichtig:** Port 465 verwendet implicit TLS (nicht STARTTLS). --- ## 11. Backup und Restore ### 11.1 Backup erstellen ```bash pg_dump -U crm_user -d crm_db -F c -f crm_backup.dump ``` ### 11.2 Restore ```bash # 1. Leere Datenbank erstellen createdb -U crm_user crm_restore # 2. Restore (ohne ACLs, ohne Owner) pg_restore -U crm_user -d crm_restore --no-owner --no-acl < crm_backup.dump # 3. Grants neu anwenden (pg_restore --no-acl überspringt Grants) # Führe Migration 0085 Grants aus oder verwende das Grant-Skript # 4. Alembic auf neuesten Stand bringen alembic upgrade head # 5. App gegen die wiederhergestellte DB starten und verifizieren ``` ### 11.3 WICHTIG: Grants nach Restore `pg_restore --no-acl` überspringt GRANT-Statements. Nach einem Restore müssen die Grants aus Migration 0085 neu angewendet werden. Alternativ: `pg_restore` ohne `--no-acl` verwenden (erfordert korrekte Rollen). --- ## 12. Häufige Probleme ### Problem: "extension vector is not available" **Ursache:** DB-Image ist `postgres:16-alpine` statt `pgvector/pgvector:pg16` **Lösung:** DB-Image in docker-compose.yml ändern ### Problem: "permission denied for schema public" **Ursache:** crm_api versucht DDL auszuführen **Lösung:** Plugin-Migrationen müssen über `get_migration_engine()` laufen (bereits implementiert) ### Problem: "MIGRATION_DATABASE_URL is not set" **Ursache:** MIGRATION_DATABASE_URL fehlt in Environment-Variablen **Lösung:** MIGRATION_DATABASE_URL setzen (auf crm_user für Bootstrap) ### Problem: Worker crasht beim Start **Ursache:** PluginModel.is_active existiert nicht (alte Migration) **Lösung:** Sicherstellen dass alle Migrationen bis 0090 ausgeführt wurden ### Problem: Login gibt 401 zurück **Ursache:** crm_auth hat keine SELECT-Rechte auf users/tenants **Lösung:** Migration 0085 Grants prüfen, ggf. neu anwenden ### Problem: RLS zeigt alle Daten ohne Tenant-Kontext **Ursache:** FORCE RLS nicht aktiviert oder Rolle ist SUPERUSER **Lösung:** `ALTER TABLE ... FORCE ROW LEVEL SECURITY` und Rolle NOSUPERUSER setzen --- ## 13. Architektur-Übersicht ``` ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ API (crm_api) │ │ Auth (crm_auth) │ │ Worker (crm_worker)│ │ SELECT/INSERT/ │ │ Login/Logout │ │ ARQ-Jobs/Outbox │ │ UPDATE/DELETE │ │ Tenant-Auflösung │ │ Cron-Jobs │ │ Audit-Log │ │ Password-Reset │ │ Event-Handler │ └────────┬─────────┘ └────────┬─────────┘ └────────┬───────────┘ │ │ │ │ ┌──────────────┐ │ │ └───┤ PostgreSQL ├──────┘────────────────────────┘ │ (RLS aktiv) │ │ 108 Tabellen│ │ Fail-closed │ └──────┬───────┘ │ ┌──────┴───────┐ │ Migration │ │ (crm_migration)│ │ BYPASSRLS │ │ DDL-Operationen│ └──────────────┘ ``` --- ## 14. Datei-Struktur ``` leocrm/ ├── app/ │ ├── core/ │ │ ├── db/__init__.py # DB-Engines (api, auth, worker, migration) │ │ ├── worker.py # ARQ-Worker-Konfiguration │ │ ├── outbox.py # Transactional Outbox │ │ ├── auth.py # Authentifizierung │ │ └── middleware.py # CSRF, CORS, Tenant-Context │ ├── plugins/ # Built-in Plugins │ ├── routes/ # API-Routes │ ├── services/ # Business-Logic │ └── models/ # SQLAlchemy-Models ├── alembic/versions/ # Migrationen 0001-0090 ├── frontend/ # React 18 + TypeScript + Vite ├── scripts/ │ ├── seed_admin.py # Admin-User erstellen │ └── test_migrations.sh # Migrations-Test ├── prestart.sh # Container-Entrypoint (API) ├── worker.sh # Container-Entrypoint (Worker) ├── healthcheck.sh # Health-Check-Script ├── docker-compose.yml # Compose-Referenz ├── .env.docker.example # ENV-Template ├── Dockerfile # Multi-Stage-Build └── requirements.txt # Python-Abhängigkeiten ``` --- *Diese Anleitung wird mit jedem Release aktualisiert. Stand: Commit be20a85, Alembic-Head 0090.*