Files
leocrm/docs/INSTALL.md
T
Agent Zero 5b7d93cd0e refactor(deploy): remove old multi-resource code, document single docker-compose workflow
- Remove POSTGRES_COMPOSE, REDIS_COMPOSE templates (unused)
- Remove create_service(), create_api_application(), generate_worker_compose()
- Remove deploy_worker(), deploy_worker_only(), verify_worker_service()
- Remove resolve_worker_uuid() and all worker_uuid references
- Remove get_worker_envs(), get_api_envs(), get_postgres_envs(), get_redis_envs()
- Remove set_service_envs(), set_application_envs() (dead code)
- Remove _extract_deploy_uuid(), _wait_service_healthy() (only used by deploy_worker)
- Remove seed_admin_user() (only used by old deploy_full)
- Remove DB_HOST, REDIS_HOST, WORKER_UUID, WORKER_NAME config vars
- Remove --worker-only CLI arg
- Replace old deploy_full() with simple redeploy via /api/v1/deploy
- Update run_verification() to remove worker_uuid param
- Add KI workflow comment at top of deploy.py
- Update DEPLOY.md: single docker-compose stack workflow
- Update COOLIFY_SETUP.md: single docker-compose stack, remove 3-resource setup
- Update docs/INSTALL.md: automated --initial workflow

deploy.py: 1370 → 893 lines (-477 lines, -35%)
2026-08-06 01:22:09 +02:00

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>@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 <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@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 <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:
  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

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:

  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)

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:

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.