17 KiB
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
git clone https://forgejo.media-on.de/Leopoldadmin/leocrm.git
cd leocrm
git checkout main
2. Docker-Image bauen
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:<PASSWORT>@postgres: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@postgres:5432/crm_db | API-Abfragen (crm_api) |
| AUTH_DATABASE_URL | postgresql+asyncpg://crm_auth:PW@postgres:5432/crm_db | Login/Auth (crm_auth) |
| WORKER_DATABASE_URL | postgresql+asyncpg://crm_worker:PW@postgres:5432/crm_db | Worker-Jobs (crm_worker) |
| MIGRATION_DATABASE_URL | postgresql+asyncpg://crm_user:PW@postgres:5432/crm_db | Migrationen (crm_user für Bootstrap) |
| REDIS_URL | redis://default:PW@redis:6379/0 | Redis-Verbindung |
| SECRET_KEY | <mindestens 32 Zeichen> | 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@postgres:5432/crm_db | Worker-DB (crm_worker) |
| WORKER_DATABASE_URL | postgresql+asyncpg://crm_worker:PW@postgres:5432/crm_db | Worker-DB (crm_worker) |
| MIGRATION_DATABASE_URL | postgresql+asyncpg://crm_user:PW@postgres:5432/crm_db | Plugin-Migrationen (crm_user) |
| REDIS_URL | redis://default:PW@redis:6379/0 | Redis-Verbindung |
| SECRET_KEY | <mindestens 32 Zeichen> | 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
version: '3.8'
services:
postgres:
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}@postgres:5432/crm_db
AUTH_DATABASE_URL: postgresql+asyncpg://crm_auth:${DB_PASSWORD}@postgres:5432/crm_db
WORKER_DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@postgres:5432/crm_db
MIGRATION_DATABASE_URL: postgresql+asyncpg://crm_user:${DB_PASSWORD}@postgres: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:
postgres:
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}@postgres:5432/crm_db
WORKER_DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@postgres:5432/crm_db
MIGRATION_DATABASE_URL: postgresql+asyncpg://crm_user:${DB_PASSWORD}@postgres:5432/crm_db
REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0
SECRET_KEY: ${SECRET_KEY}
ENVIRONMENT: production
STORAGE_PATH: /data/storage
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
volumes:
db-data:
redis-data:
api-storage:
.env Datei
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.
# 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="<project-uuid>"
export COOLIFY_SERVER_UUID="<server-uuid>"
export COOLIFY_PRIVATE_KEY_UUID="<private-key-uuid>"
# Initial deployment (2-Phase: ohne Domain, dann mit Domain + Redeploy)
python scripts/deploy.py --initial
Das Script macht automatisch:
- Coolify Application via
private-deploy-keyerstellen - Build Pack auf
dockercomposesetzen (liest docker-compose.yaml aus Git) - Environment-Variablen setzen (Secrets, Domain, Admin-Credentials)
- Erster Deploy (ohne Domain — Coolify muss docker-compose.yaml lesen)
docker_compose_domainssetzen + Redeploy (mit Traefik-Labels)- Verifikation (HTTP, Login, Alembic, RLS)
6.2 Redeploy (bestehende Anwendung)
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
:443indocker_compose_domains—:443fü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:pg16sein (nichtpostgres: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:
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
curl https://crm.example.com/api/v1/health
# Erwartet: {"status":"healthy",...}
9.2 Login-Test
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
docker exec api-container python3 -m alembic current
# Erwartet: 0090 (head)
9.4 RLS prüfen
-- 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 = '<tenant-uuid>';
SELECT count(*) FROM contacts; -- Erwartet: > 0
RESET ROLE;
9.5 DDL durch crm_api blockiert
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
pg_dump -U crm_user -d crm_db -F c -f crm_backup.dump
11.2 Restore
# 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.