tva
← Insights

Docker für mehrere Mandanten ohne unbeabsichtigte gemeinsame Ausfälle

Ein aktueller, produktionsnaher Folgebeitrag, der Herstellerdokumentation in Betriebskontrollen, Migrationsentscheidungen und überprüfbare Freigabekriterien übersetzt.

Was sich 2026 geändert hat

Wir greifen das Thema erneut auf, weil sich die Systemgrenzen verschoben haben. Die dauerhaften Prinzipien bleiben wertvoll, doch aktuelle Versionen machen einige frühere Abkürzungen unvollständig oder riskant. Dieser Folgebeitrag beginnt mit der am 14. Juli 2026 verfügbaren Herstellerdokumentation, trennt Fakten von lokalen Entscheidungen und behandelt jede Konfigurationsänderung als kontrollierten Eingriff in den Produktivbetrieb.

Aktuelle Primärquellen

So setzen wir die Aktualisierung ein

Zuerst werden die Primärquellen geprüft und die tatsächlich eingesetzten Versionen dokumentiert. Vor jeder Änderung definieren wir das beobachtbare Ergebnis. Der kleinste reversible Eingriff wird in einer repräsentativen Umgebung getestet. Ein erfolgreicher Befehl ist noch kein Abnahmekriterium: Entscheidend sind Dienstzustand, Datenintegrität, Latenz, Sicherheitsgrenzen und Rücksetzzeit. Die weiterhin nützliche Diagnoselogik des früheren Beitrags bleibt nachfolgend als betriebliche Grundlage erhalten; versionsabhängige Beispiele müssen mit der aktuellen Dokumentation abgeglichen werden.

Betriebliche Grundlage

So erstellen Sie eine vorlagenbasierte Entwicklungsumgebung für mehrere Mandanten mit 16 containerisierten Diensten, die offline funktioniert und online über subdomainbasiertes Routing erreichbar bleibt. Die folgenden Konfigurationsdateien, Skripte und Befehlsbeispiele stammen aus der ursprünglichen Anleitung und wurden wo nötig korrigiert.

Die Verwaltung von Entwicklungsumgebungen für mehrere Kunden bedeutet oft die Wahl zwischen komplexen manuellen Einrichtungen und teuren Cloud-Lösungen. Manuelle Deployments sind zeitaufwendig und fehleranfällig. Cloud-Plattformen sind bequem, schaffen aber eine Anbieterbindung und laufende Kosten, die mit der Nutzung steigen.

Heute führen wir Sie durch den Aufbau eines skalierbaren Entwicklungsstacks für mehrere Mandanten. Er bietet starke Trennung zwischen Kundenumgebungen und automatisierte Deployments bei voller Kontrolle über Ihre Infrastruktur. Dieser Ansatz baut auf unserer Philosophie selbst betriebener Lösungen auf – ähnlich wie unsere Anleitungen zum Self-Hosting von n8n für Workflow-Automatisierung und zum Deployment von Windmill mit Docker für vollständige Kontrolle über den Betrieb.

Die von uns verwendeten Tools

Beginnen wir damit zu verstehen, was jede Komponente in unserer umfassenden 16-Container-Architektur leistet:

Docker: Die Grundlage der Containerisierung

Docker bietet die Trennung und Konsistenz, die wir für mandantenfähige Umgebungen benötigen. Jeder Kunde erhält eigene Container mit identischen Konfigurationen. Das trägt dazu bei, dass das, was in der Entwicklung funktioniert, auch im Produktivbetrieb funktioniert. Stellen Sie sich mehrere getrennte Server auf derselben Hardware vor, die sich jedoch weiterhin CPU, Arbeitsspeicher, Festplatte und Kernel teilen.

Der entscheidende Vorteil ist die starke Trennung zwischen Kunden. Daten, Konfigurationen und Anpassungen jedes Kunden liegen in separaten Containern, Netzwerken und Volumes, sodass sie die anderer Kunden nicht beeinflussen. Das ist wichtig, wenn Sie mehrere Geschäftskunden mit unterschiedlichen Anforderungen und Sicherheitsbedürfnissen betreuen.

Traefik: Intelligenter Reverse Proxy und Load Balancer

Traefik lenkt den Datenverkehr und leitet Anfragen anhand der Domainnamen automatisch an die richtige Kundenumgebung weiter. Statt komplexe Apache- oder Nginx-Regeln manuell zu konfigurieren, lassen Sie Traefik die Labels Ihrer Docker-Container lesen und das Routing automatisch einrichten.

Stellen Sie sich Traefik als eine Empfangskraft vor, die genau weiß, in welches Büro (Container) jeder Besucher (Anfrage) gehört, ohne jedes Mal Anweisungen zu benötigen. In unserem Setup übernimmt Traefik die SSL-Terminierung und die automatische Erkennung von Diensten und stellt detaillierte Monitoring-Dashboards bereit.

Cloudflare Tunnels: Sicherer externer Zugriff

Cloudflare Tunnels bieten sicheren Zugriff auf Ihren lokalen Entwicklungsstack ohne komplexe Firewall-Konfigurationen oder VPNs. Jede Kundendomain erhält ihren eigenen Tunnel, wodurch die externen Zugriffswege der Kunden getrennt bleiben.

Das Elegante ist, dass Ihre Entwicklungsumgebungen lokal und sicher bleiben, aber Kunden von überall mit entsprechender Authentifizierung auf ihre spezifischen Dienste zugreifen können – ähnlich wie wir den sicheren externen Zugriff in unserem n8n-Hosting-Leitfaden konfiguriert haben.

Der vollständige Dienstestack: Alles, was Ihre Kunden brauchen

Unser Stack für mehrere Mandanten umfasst vier Kategorien zentraler Dienste mit insgesamt 16 Containern pro Kunde:

Workflow-Automatisierung und Geschäftslogik:

  • n8n: Vollständige Workflow-Automatisierungsplattform für die Automatisierung von Geschäftsprozessen
  • Authentik: Single-Sign-On und Identitätsmanagement auf Unternehmensniveau (3 Container: Server, Worker, Redis-Cache)

Datenbank- und Backend-Dienste:

  • PostgreSQL: Robustes Datenbank-Backend für alle Dienste mit optimierter Wiederverwendung von Verbindungen
  • Supabase-Stack: Vollständiges Backend-as-a-Service mit 6 spezialisierten Containern (Studio, Meta, Auth, REST API, Realtime, Kong Gateway)
  • NocoDB: No-Code-Datenbankoberfläche für die Kundendatenverwaltung

KI und intelligente Funktionen:

  • Ollama: Lokale KI-Sprachmodelle mit GPU-Beschleunigung für intelligente Automatisierung
  • Qdrant (optional): Vektordatenbank für fortgeschrittene KI-Workflows und Ähnlichkeitssuche

Infrastruktur und Monitoring:

  • Cloudflare Tunnel: Sichere externe Konnektivität
  • Traefik: Reverse Proxy mit automatischem SSL und Monitoring-Dashboard

Wie alles zusammenwirkt

Hier ist der vollständige Ablauf, wenn ein Kunde auf seine Umgebung zugreift:

  • Der Kunde navigiert zu seiner benutzerdefinierten Domain (z. B. workflows.client-a.com)
  • Cloudflare Tunnel leitet die Anfrage an Ihre lokale Traefik-Instanz weiter
  • Traefik liest die Domain, wendet Middleware an (Authentifizierung, SSL, Aufruflimits) und leitet an den korrekten Kundencontainer weiter
  • Authentik übernimmt die SSO-Authentifizierung über alle Dienste hinweg, falls konfiguriert
  • Der Kunde erhält seine eigene isolierte Umgebung mit seinen Daten und Konfigurationen
  • Die Umgebungen anderer Kunden sind über die Routen dieses Kunden nicht erreichbar

Alles bleibt organisiert und getrennt, wobei jeder Kunde seine eigene Subdomain-Struktur erhält wie auth.client-a.com, database.client-a.com, backend.client-a.com usw.

Die Einrichtung: Die praktischen Schritte

Das Fundament vorbereiten

Zunächst benötigen Sie Docker Desktop und eine eingerichtete Domainverwaltung. Wir empfehlen eine Wildcard-DNS-Struktur, um neue Kunden einfach einzubinden:

# Install Docker Desktop (macOS)
brew install --cask docker

# Verify installation
docker --version
docker compose version

# Ensure sufficient resources for multi-container environments
# Recommended: 16GB RAM, 8+ CPU cores, 500GB+ SSD storage

Das Vorlagensystem erstellen

Entscheidend ist der vorlagenbasierte Ansatz. Statt jeden Kunden manuell einzurichten, erstellen wir Vorlagen, die sich sofort mit kundenspezifischen Konfigurationen bereitstellen lassen.

Erstellen Sie die umfassende Verzeichnisstruktur:

mkdir -p development-stack/{template,deployments}
cd development-stack/template

# Create service-specific configuration directories
mkdir -p {traefik,authentik,supabase,init}

Vollständige Vorlage für mehrere Dienste

Erstellen Sie eine vollständige docker-compose.yml-Vorlage mit allen 16 Diensten:

networks:
  tenant:
    name: ${TENANT_NETWORK}
    driver: bridge

services:
  # External Connectivity (public hostnames point to https://traefik:443 in the Cloudflare dashboard)
  cloudflare-tunnel:
    image: cloudflare/cloudflared:2026.7.2
    container_name: ${TENANT_PREFIX}-tunnel
    command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TOKEN}
    networks:
      - tenant
    restart: unless-stopped

  # Reverse Proxy & Load Balancer
  # No host ports 80/443: client traffic arrives through the tunnel, so several
  # clients can share one host. Certificates use the Cloudflare DNS challenge.
  traefik:
    image: traefik:v3.7.9
    container_name: ${TENANT_PREFIX}-traefik
    command:
      - "--api.dashboard=true"
      - "--ping=true"
      - "--providers.docker=true"
      - "--providers.docker.network=${TENANT_NETWORK}"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.constraints=Label(`com.docker.compose.project`,`${TENANT_PREFIX}`)"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.websecure.asdefault=true"
      - "--entrypoints.traefik.address=:8080"
      - "--certificatesresolvers.letsencrypt.acme.email=${ADMIN_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
    environment:
      CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN}
    ports:
      # Dashboard on localhost only (http://localhost:PORT/dashboard/)
      - "127.0.0.1:${TRAEFIK_DASHBOARD_PORT}:8080"
    networks:
      - tenant
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/acme.json:/acme.json
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=PathPrefix(`/`)"
      - "traefik.http.routers.dashboard.entrypoints=traefik"
      - "traefik.http.routers.dashboard.service=api@internal"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Database Backend
  postgres:
    image: postgres:15.18-alpine
    container_name: ${TENANT_PREFIX}-postgres
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    networks:
      - tenant
    ports:
      # Localhost only, for admin tools on the host
      - "127.0.0.1:${POSTGRES_PORT}:5432"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 30s
      timeout: 10s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 1G

  # Workflow Automation
  n8n:
    image: n8nio/n8n:2.31.7
    container_name: ${TENANT_PREFIX}-n8n
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_PROTOCOL: ${N8N_PROTOCOL}
      N8N_HOST: ${N8N_DOMAIN}
      N8N_PORT: 5678
      N8N_SECURE_COOKIE: ${N8N_SECURE_COOKIE}
      WEBHOOK_URL: https://${N8N_DOMAIN}
      N8N_EDITOR_BASE_URL: https://${N8N_DOMAIN}
      EXECUTIONS_DATA_PRUNE: "true"
      EXECUTIONS_DATA_MAX_AGE: 168
    volumes:
      - n8n_data:/home/node/.n8n
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.n8n.rule=Host(`${N8N_DOMAIN}`)"
      - "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
      - "traefik.http.services.n8n.loadbalancer.server.port=5678"
      - "traefik.http.routers.n8n.middlewares=${AUTH_MIDDLEWARE}"
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

  # No-Code Database Interface
  nocodb:
    image: nocodb/nocodb:2026.07.0
    container_name: ${TENANT_PREFIX}-nocodb
    environment:
      NC_DB: "pg://postgres:5432?u=${POSTGRES_USER}&p=${POSTGRES_PASSWORD}&d=nocodb"
      NC_PUBLIC_URL: https://${NOCODB_DOMAIN}
      NC_DISABLE_TELE: "true"
      NC_ADMIN_EMAIL: ${ADMIN_EMAIL}
      NC_ADMIN_PASSWORD: ${NOCODB_ADMIN_PASSWORD}
    volumes:
      - nocodb_data:/usr/app/data
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nocodb.rule=Host(`${NOCODB_DOMAIN}`)"
      - "traefik.http.routers.nocodb.tls.certresolver=letsencrypt"
      - "traefik.http.services.nocodb.loadbalancer.server.port=8080"
      - "traefik.http.routers.nocodb.middlewares=${AUTH_MIDDLEWARE}"
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

  # Supabase Backend Stack (5 containers)
  supabase-studio:
    image: supabase/studio:2026.07.27-sha-cbb076d
    container_name: ${TENANT_PREFIX}-supabase-studio
    environment:
      STUDIO_PG_META_URL: http://supabase-meta:8080
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      DEFAULT_ORGANIZATION_NAME: ${CLIENT_NAME}
      DEFAULT_PROJECT_NAME: ${CLIENT_NAME} Project
      SUPABASE_PUBLIC_URL: https://${SUPABASE_DOMAIN}
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.supabase-studio.rule=Host(`${SUPABASE_DOMAIN}`)"
      - "traefik.http.routers.supabase-studio.tls.certresolver=letsencrypt"
      - "traefik.http.services.supabase-studio.loadbalancer.server.port=3000"
      - "traefik.http.routers.supabase-studio.middlewares=${AUTH_MIDDLEWARE}"
    restart: unless-stopped
    healthcheck:
      disable: true
    depends_on:
      postgres:
        condition: service_healthy

  supabase-meta:
    image: supabase/postgres-meta:v0.96.6
    container_name: ${TENANT_PREFIX}-supabase-meta
    environment:
      PG_META_PORT: 8080
      PG_META_DB_HOST: postgres
      PG_META_DB_PORT: 5432
      PG_META_DB_NAME: supabase
      PG_META_DB_USER: ${POSTGRES_USER}
      PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
    networks:
      - tenant
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

  supabase-auth:
    image: supabase/gotrue:v2.193.1
    container_name: ${TENANT_PREFIX}-supabase-auth
    environment:
      GOTRUE_API_HOST: 0.0.0.0
      GOTRUE_API_PORT: 9999
      API_EXTERNAL_URL: https://api.${CLIENT_DOMAIN}
      GOTRUE_DB_DRIVER: postgres
      GOTRUE_DB_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/supabase
      GOTRUE_SITE_URL: https://${SUPABASE_DOMAIN}
      GOTRUE_JWT_SECRET: ${SUPABASE_JWT_SECRET}
      GOTRUE_JWT_EXP: 3600
      GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
    networks:
      - tenant
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

  supabase-rest:
    image: postgrest/postgrest:v13.0.8
    container_name: ${TENANT_PREFIX}-supabase-rest
    environment:
      PGRST_DB_URI: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/supabase
      PGRST_DB_SCHEMAS: public
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET}
      PGRST_DB_USE_LEGACY_GUCS: "false"
    networks:
      - tenant
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

  supabase-realtime:
    image: supabase/realtime:v2.120.3
    container_name: ${TENANT_PREFIX}-supabase-realtime
    environment:
      PORT: 4000
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: ${POSTGRES_USER}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_NAME: supabase
      DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
      DB_ENC_KEY: supabaserealtime
      API_JWT_SECRET: ${SUPABASE_JWT_SECRET}
      SECRET_KEY_BASE: ${SUPABASE_JWT_SECRET}
      METRICS_JWT_SECRET: ${SUPABASE_JWT_SECRET}
      ERL_AFLAGS: -proto_dist inet_tcp
      DNS_NODES: "''"
      APP_NAME: realtime
      SEED_SELF_HOST: "true"
    networks:
      - tenant
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy

  supabase-kong:
    image: kong:3.9.1
    container_name: ${TENANT_PREFIX}-supabase-kong
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
      KONG_DNS_ORDER: LAST,A,CNAME
      KONG_PLUGINS: request-size-limiting,cors,key-auth,rate-limiting
      KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
      KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
    volumes:
      - ./supabase/kong.yml:/var/lib/kong/kong.yml:ro
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.kong.rule=Host(`api.${CLIENT_DOMAIN}`)"
      - "traefik.http.routers.kong.tls.certresolver=letsencrypt"
      - "traefik.http.services.kong.loadbalancer.server.port=8000"
      - "traefik.http.routers.kong.middlewares=${AUTH_MIDDLEWARE}"
    restart: unless-stopped

  # Local AI Language Models
  ollama:
    image: ollama/ollama:0.32.3
    container_name: ${TENANT_PREFIX}-ollama
    environment:
      OLLAMA_HOST: 0.0.0.0:11434
      OLLAMA_ORIGINS: "*"
    volumes:
      - ollama_data:/root/.ollama
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.ollama.rule=Host(`ai.${CLIENT_DOMAIN}`)"
      - "traefik.http.routers.ollama.tls.certresolver=letsencrypt"
      - "traefik.http.services.ollama.loadbalancer.server.port=11434"
      - "traefik.http.routers.ollama.middlewares=${AUTH_MIDDLEWARE}"
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 16G
        reservations:
          memory: 8G
    healthcheck:
      test: ["CMD", "ollama", "list"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Enterprise SSO Authentication (3 containers)
  authentik-redis:
    image: redis:7.4.9-alpine
    container_name: ${TENANT_PREFIX}-authentik-redis
    command: --save 60 1 --loglevel warning
    networks:
      - tenant
    volumes:
      - authentik_redis_data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 30s
      timeout: 3s
      retries: 3

  authentik-server:
    image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG}
    container_name: ${TENANT_PREFIX}-authentik-server
    command: server
    environment:
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
      AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
      AUTHENTIK_POSTGRESQL__HOST: postgres
      AUTHENTIK_POSTGRESQL__USER: ${POSTGRES_USER}
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${POSTGRES_PASSWORD}
      AUTHENTIK_REDIS__HOST: authentik-redis
    volumes:
      - authentik_media:/media
      - authentik_templates:/templates
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authentik.rule=Host(`auth.${CLIENT_DOMAIN}`)"
      - "traefik.http.routers.authentik.tls.certresolver=letsencrypt"
      - "traefik.http.services.authentik.loadbalancer.server.port=9000"
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      authentik-redis:
        condition: service_healthy

  authentik-worker:
    image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG}
    container_name: ${TENANT_PREFIX}-authentik-worker
    command: worker
    environment:
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
      AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
      AUTHENTIK_POSTGRESQL__HOST: postgres
      AUTHENTIK_POSTGRESQL__USER: ${POSTGRES_USER}
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${POSTGRES_PASSWORD}
      AUTHENTIK_REDIS__HOST: authentik-redis
    volumes:
      - authentik_media:/media
      - authentik_templates:/templates
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - tenant
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      authentik-redis:
        condition: service_healthy

  # Test Service for Health Monitoring
  whoami:
    image: traefik/whoami:v1.11.0
    container_name: ${TENANT_PREFIX}-whoami
    networks:
      - tenant
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`test.${CLIENT_DOMAIN}`)"
      - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"
    restart: unless-stopped

volumes:
  postgres_data:
  n8n_data:
  nocodb_data:
  ollama_data:
  authentik_redis_data:
  authentik_media:
  authentik_templates:

Vollständige Umgebungsvorlage

Erstellen Sie .env.template mit allen benötigten kundenspezifischen Variablen:

# Client Configuration
CLIENT_NAME=CLIENT_NAME_PLACEHOLDER
CLIENT_DOMAIN=CLIENT_DOMAIN_PLACEHOLDER
TENANT_PREFIX=CLIENT_PREFIX_PLACEHOLDER
TENANT_NETWORK=CLIENT_NETWORK_PLACEHOLDER
# Compose project name: keeps volume names and Traefik's container filter per client
COMPOSE_PROJECT_NAME=CLIENT_PREFIX_PLACEHOLDER

# Service Domains (Subdomain-based routing)
N8N_DOMAIN=workflows.CLIENT_DOMAIN_PLACEHOLDER
NOCODB_DOMAIN=database.CLIENT_DOMAIN_PLACEHOLDER
SUPABASE_DOMAIN=backend.CLIENT_DOMAIN_PLACEHOLDER
AUTHENTIK_DOMAIN=auth.CLIENT_DOMAIN_PLACEHOLDER

# Host Ports (localhost only, offset per client by the deployment script)
TRAEFIK_DASHBOARD_PORT=8080
POSTGRES_PORT=5432

# Admin Configuration
ADMIN_EMAIL=admin@CLIENT_DOMAIN_PLACEHOLDER

# Database Configuration
POSTGRES_DB=main_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=SECURE_PASSWORD_PLACEHOLDER

# Service-Specific Passwords
NOCODB_ADMIN_PASSWORD=NOCODB_PASSWORD_PLACEHOLDER
SUPABASE_JWT_SECRET=SUPABASE_JWT_PLACEHOLDER

# Authentik SSO Configuration
AUTHENTIK_SECRET_KEY=AUTHENTIK_SECRET_PLACEHOLDER
AUTHENTIK_TAG=2025.8.6

# n8n Configuration
N8N_PROTOCOL=https
N8N_SECURE_COOKIE=true

# Cloudflare Integration (tunnel token, and an API token with Zone:DNS:Edit for certificates)
CLOUDFLARE_TOKEN=CLOUDFLARE_TOKEN_PLACEHOLDER
CF_DNS_API_TOKEN=CF_DNS_API_TOKEN_PLACEHOLDER

# Authentication Middleware (set to 'auth-global' for SSO, leave empty for no auth)
AUTH_MIDDLEWARE=

Skripte zur Datenbankinitialisierung

Erstellen Sie eine umfassende Datenbankinitialisierung in init/01-create-multiple-databases.sql:

-- Create databases for all services
CREATE DATABASE n8n;
CREATE DATABASE nocodb;
CREATE DATABASE supabase;
CREATE DATABASE authentik;

-- Enable required extensions for Supabase
-- (pgjwt is not part of the official postgres image, and a failing statement aborts the init)
\c supabase
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Schemas and role the Supabase services expect
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS _realtime;
CREATE ROLE anon NOLOGIN;
GRANT USAGE ON SCHEMA public TO anon;

-- Enable required extensions for Authentik
\c authentik
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

\echo 'Multiple databases and extensions created successfully'

Supabase-Kong-Gateway-Konfiguration

Erstellen Sie supabase/kong.yml für das API-Gateway-Routing:

_format_version: "3.0"

services:
  - name: auth-v1-open
    url: http://supabase-auth:9999/verify
    plugins:
      - name: cors
    routes:
      - name: auth-v1-open
        strip_path: true
        paths:
          - /auth/v1/verify
        methods:
          - POST
          - OPTIONS

  - name: auth-v1-open-callback
    url: http://supabase-auth:9999/callback
    plugins:
      - name: cors
    routes:
      - name: auth-v1-open-callback
        strip_path: true
        paths:
          - /auth/v1/callback
        methods:
          - GET
          - POST
          - OPTIONS

  - name: auth-v1
    _comment: "GoTrue: /auth/v1/* -> http://supabase-auth:9999/*"
    url: http://supabase-auth:9999/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: false
    routes:
      - name: auth-v1-all
        strip_path: true
        paths:
          - /auth/v1/
        methods:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE
          - OPTIONS

  - name: rest-v1
    _comment: "PostgREST: /rest/v1/* -> http://supabase-rest:3000/*"
    url: http://supabase-rest:3000/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: true
    routes:
      - name: rest-v1-all
        strip_path: true
        paths:
          - /rest/v1/
        methods:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE
          - OPTIONS

  - name: realtime-v1
    _comment: "Realtime: /realtime/v1/* -> ws://supabase-realtime:4000/socket/*"
    url: http://supabase-realtime:4000/socket/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: false
    routes:
      - name: realtime-v1-all
        strip_path: true
        paths:
          - /realtime/v1/
        methods:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE
          - OPTIONS

consumers:
  - username: anon
    keyauth_credentials:
      - key: your-anon-key-here
  - username: service_role
    keyauth_credentials:
      - key: your-service-role-key-here

plugins:
  - name: cors
    config:
      origins:
        - "*"
      methods:
        - GET
        - POST
        - PUT
        - PATCH
        - DELETE
        - OPTIONS
      headers:
        - Accept
        - Accept-Version
        - Content-Length
        - Content-MD5
        - Content-Type
        - Date
        - X-Auth-Token
        - Authorization
        - X-Forwarded-For
        - X-Forwarded-Proto
        - X-Forwarded-Port
      exposed_headers:
        - X-Auth-Token
      credentials: true
      max_age: 3600

Automatisiertes Bereitstellungsskript

Dieses umfassende Bereitstellungsskript erstellt neue Kundenumgebungen in wenigen Minuten:

#!/bin/bash
# deploy-client.sh - Complete Multi-Tenant Deployment
set -euo pipefail

CLIENT_DOMAIN=${1:-}
CLIENT_NAME=${2:-}
CLOUDFLARE_TOKEN=${3:-}

if [ -z "$CLIENT_DOMAIN" ] || [ -z "$CLIENT_NAME" ] || [ -z "$CLOUDFLARE_TOKEN" ] || [ -z "${CF_DNS_API_TOKEN:-}" ]; then
    echo "Usage: CF_DNS_API_TOKEN=... ./deploy-client.sh example.com 'Client Name' 'cloudflare-tunnel-token'"
    echo ""
    echo "Example: ./deploy-client.sh client-a.com 'Client A Corporation' 'your-cloudflare-token'"
    echo "CF_DNS_API_TOKEN: Cloudflare API token with Zone:DNS:Edit, used for certificates"
    exit 1
fi

# Run from the template directory, wherever the script is called from
cd "$(dirname "$0")"

CLIENT_PREFIX=$(echo "$CLIENT_DOMAIN" | sed 's/[.-]//g' | tr '[:upper:]' '[:lower:]')
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

echo "🚀 Deploying complete multi-tenant environment..."
echo "📋 Configuration:"
echo "   Domain: $CLIENT_DOMAIN"
echo "   Name: $CLIENT_NAME"
echo "   Prefix: $CLIENT_PREFIX"
echo "   Timestamp: $TIMESTAMP"
echo ""

# Create deployment directory
DEPLOY_DIR="../deployments/$CLIENT_DOMAIN"
mkdir -p "$DEPLOY_DIR"/{traefik,authentik,supabase,init,logs}

echo "📁 Created deployment directory structure"

# Copy template files
cp docker-compose.yml "$DEPLOY_DIR/"
cp -R traefik authentik supabase init "$DEPLOY_DIR/"

echo "📋 Copied configuration templates"

# Generate secure passwords and secrets
POSTGRES_PASSWORD=$(openssl rand -hex 16)
NOCODB_PASSWORD=$(openssl rand -hex 12)
SUPABASE_JWT_SECRET=$(openssl rand -hex 32)
AUTHENTIK_SECRET=$(openssl rand -hex 32)

echo "🔐 Generated secure credentials"

# Offset the localhost-only host ports per client (80/443 are not published at all)
PORT_OFFSET=$(($(echo "$CLIENT_PREFIX" | cksum | cut -f1 -d' ') % 1000))
TRAEFIK_DASHBOARD_PORT=$((8080 + PORT_OFFSET))
POSTGRES_PORT=$((5432 + PORT_OFFSET))

# Create comprehensive environment file (readable by the owner only)
umask 077
sed -e "s/CLIENT_NAME_PLACEHOLDER/$CLIENT_NAME/g" \
    -e "s/CLIENT_DOMAIN_PLACEHOLDER/$CLIENT_DOMAIN/g" \
    -e "s/CLIENT_PREFIX_PLACEHOLDER/$CLIENT_PREFIX/g" \
    -e "s/CLIENT_NETWORK_PLACEHOLDER/${CLIENT_PREFIX}-network/g" \
    -e "s/SECURE_PASSWORD_PLACEHOLDER/$POSTGRES_PASSWORD/g" \
    -e "s/NOCODB_PASSWORD_PLACEHOLDER/$NOCODB_PASSWORD/g" \
    -e "s/SUPABASE_JWT_PLACEHOLDER/$SUPABASE_JWT_SECRET/g" \
    -e "s/AUTHENTIK_SECRET_PLACEHOLDER/$AUTHENTIK_SECRET/g" \
    -e "s/CLOUDFLARE_TOKEN_PLACEHOLDER/$CLOUDFLARE_TOKEN/g" \
    -e "s/CF_DNS_API_TOKEN_PLACEHOLDER/$CF_DNS_API_TOKEN/g" \
    -e "s/^TRAEFIK_DASHBOARD_PORT=.*/TRAEFIK_DASHBOARD_PORT=$TRAEFIK_DASHBOARD_PORT/" \
    -e "s/^POSTGRES_PORT=.*/POSTGRES_PORT=$POSTGRES_PORT/" \
    .env.template > "$DEPLOY_DIR/.env"

echo "⚙️  Generated environment configuration"

# Create ACME file for Traefik SSL
touch "$DEPLOY_DIR/traefik/acme.json"
chmod 600 "$DEPLOY_DIR/traefik/acme.json"

# Initialize deployment
cd "$DEPLOY_DIR"

echo "🐋 Starting Docker containers..."
echo "   This may take several minutes for first-time image downloads"

# Start all services (each waits for the database health check)
docker compose up -d

echo ""
echo "✅ Multi-tenant environment deployed successfully!"
echo ""
echo "🌐 Access URLs:"
echo "   🔧 Traefik Dashboard: http://localhost:$TRAEFIK_DASHBOARD_PORT/dashboard/"
echo "   🔀 Workflows (n8n): https://workflows.$CLIENT_DOMAIN"
echo "   🗄️  Database (NocoDB): https://database.$CLIENT_DOMAIN"
echo "   🔧 Backend (Supabase): https://backend.$CLIENT_DOMAIN"
echo "   🤖 AI (Ollama): https://ai.$CLIENT_DOMAIN"
echo "   🔐 Authentication: https://auth.$CLIENT_DOMAIN"
echo "   🔌 API Gateway: https://api.$CLIENT_DOMAIN"
echo "   🧪 Test Service: https://test.$CLIENT_DOMAIN"
echo ""
echo "📊 Container Status:"
docker compose ps
echo ""
echo "🔑 Generated credentials are stored in $DEPLOY_DIR/.env (mode 600)"
echo ""
echo "📝 Next Steps:"
echo "   1. In the Cloudflare tunnel, route *.${CLIENT_DOMAIN} to https://traefik:443 (No TLS Verify)"
echo "   2. Wait 2-3 minutes for all services to initialize"
echo "   3. Access services via the URLs above"
echo "   4. Configure SSO via auth.$CLIENT_DOMAIN if needed"
echo ""
echo "📚 Documentation: Visit tva.sg for setup guides and troubleshooting"
echo "💬 Support: Contact us via tva.sg/contact for assistance"

Ihren Stack für mehrere Mandanten verwenden

Neue Kundenumgebungen bereitstellen

Mit unserem umfassenden Skript lässt sich eine neue Kundenumgebung einfach erstellen:

# Set once per shell: Cloudflare API token with Zone:DNS:Edit (used for certificates)
export CF_DNS_API_TOKEN="your-cloudflare-dns-token"

# Deploy Client A with full enterprise stack
./deploy-client.sh client-a.com "Client A Corporation" "your-cloudflare-token"

# Deploy Client B with different domain
./deploy-client.sh client-b.org "Client B Industries" "your-cloudflare-token"

# Deploy Startup C
./deploy-client.sh startup-c.io "Startup C" "your-cloudflare-token"

Jede Bereitstellung erstellt:

  • Dediziertes Docker-Netzwerk mit 16 Containern
  • Separate Daten-Volumes für persistente Speicherung
  • Eigene Dienstcontainer mit Zustandsüberwachung
  • Individuelle Cloudflare-Tunnel-Konfiguration
  • Benutzerdefiniertes Domain-Routing mit SSL-Zertifikaten
  • SSO-Infrastruktur auf Unternehmensniveau, bereit zur Aktivierung

Mehrere Umgebungen verwalten

Überwachen Sie alle Kundenumgebungen von einem zentralen Ort aus:

# Check all running environments across clients
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(client|startup)"

# View comprehensive logs for specific client
cd deployments/client-a.com
docker compose logs -f --tail=50 n8n

# Health check all services for a client
docker compose ps
docker compose exec postgres pg_isready

# Restart specific services
docker compose restart nocodb supabase-studio

# Update all services to the image versions now pinned in docker-compose.yml
docker compose pull && docker compose up -d

Container-Architektur im Detail

Unsere vollständige 16-Container-Architektur pro Kunde umfasst:

Infrastrukturschicht (4 Container):

  • cloudflare-tunnel: Sichere externe Verbindung
  • traefik: Reverse Proxy mit automatischem SSL und Erkennung von Diensten
  • postgres: Zentrale Datenbank mit Wiederverwendung von Verbindungen
  • whoami: Zustandsüberwachung und Routing-Prüfung

Anwendungsschicht (7 Container):

  • n8n: Workflow-Automatisierung mit PostgreSQL-Backend
  • nocodb: No-Code-Datenbankoberfläche
  • supabase-studio: Backend-Entwicklungs-Dashboard
  • supabase-meta: Datenbank-Introspektionsdienst
  • supabase-auth: Authentifizierung und Benutzerverwaltung
  • supabase-rest: Automatisch generierte REST-API
  • supabase-realtime: Echtzeit-Abonnements und Updates

KI- und Gateway-Schicht (2 Container):

  • ollama: Lokale KI mit GPU-Beschleunigungsunterstützung
  • supabase-kong: API-Gateway mit Aufruflimits und CORS

Sicherheitsschicht auf Unternehmensniveau (3 Container):

  • authentik-server: SSO-Authentifizierungsserver
  • authentik-worker: Hintergrundaufgaben und Benachrichtigungen
  • authentik-redis: Sitzungsverwaltung und Caching

Ressourcen pro Kunde skalieren

Passen Sie Ressourcen basierend auf Kundenbedürfnissen und Nutzungsmustern an:

# High-performance client configuration (docker-compose.override.yml)
services:
  n8n:
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 8G
        reservations:
          cpus: '2.0'
          memory: 4G

  postgres:
    command: postgres -c max_connections=200 -c shared_buffers=1GB -c effective_cache_size=3GB
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G

  ollama:
    deploy:
      resources:
        limits:
          memory: 32G
        reservations:
          memory: 16G
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

Echte Vorteile für Ihr Unternehmen

Vollständige Mandantentrennung mit Funktionen auf Unternehmensniveau

Jeder Kunde erhält eine umfassende eigene Umgebung mit SSO auf Unternehmensniveau, KI-Funktionen und vollständiger Backend-Infrastruktur. Daten, Konfigurationen, Anpassungen und Sicherheitsrichtlinien bleiben getrennt. Ein Problem auf Anwendungsebene in einer Kundenumgebung bleibt auf diese Umgebung beschränkt. Da sich jedoch alle Kunden einen Host teilen, können Probleme auf Host-Ebene wie erschöpfte CPU-, Arbeitsspeicher- oder Festplattenressourcen weiterhin alle betreffen. Dies ähnelt der Trennung unserer individuellen n8n-Deployments.

Neue Kunden schnell mit vollem Funktionsumfang einbinden

Neue Kunden können in unter 10 Minuten mit einem vollständigen Entwicklungs- und Automatisierungsstack starten. Das Bereitstellungsskript automatisiert Konfiguration, Generierung von Zugangsdaten und Initialisierung der Dienste; Cloudflare-DNS-Einträge und optionales SSO bleiben kurze manuelle Schritte.

Vorhersehbare Kosten für Unternehmensfunktionen

Nach der Ersteinrichtung fallen über die Basisinfrastruktur hinaus keine kundenspezifischen Hosting-Kosten an. Anders als bei SaaS-Lösungen, die pro Nutzer, Workflow oder API-Aufruf abrechnen, zahlen Sie einen festen Betrag für den Server und können so viele Kundenumgebungen mit vollem Funktionsumfang auf Unternehmensniveau betreiben, wie dessen Ressourcen zulassen.

Professionelle Markenkonsistenz

Jeder Kunde erhält Domains mit eigener Markenidentität und professionellen Subdomains (workflows.client.com, auth.client.com usw.) und kann seine Umgebungen vollständig anpassen. Es gibt keine „powered by“-Fußzeilen oder gemeinsam genutzten Oberflächen, die die Markenidentität verwässern.

Die n8n-Integration: Workflow-Automatisierung für Unternehmen im großen Maßstab

Hier entsteht eine besonders leistungsfähige Kombination. Wie in unserer Anleitung zum Self-Hosting von n8n für Workflow-Automatisierung erhält mit diesem Setup jeder Kunde seine eigene vollständige n8n-Instanz, eingebunden in einen kompletten Stack für Unternehmen.

Jeder Kunde kann anspruchsvolle Workflows erstellen, die:

  • sich mit den eigenen Datenbanken verbinden (NocoDB, Supabase PostgreSQL)
  • eigene KI-Modelle (Ollama) für intelligente Automatisierung nutzen
  • sich über SSO auf Unternehmensniveau (Authentik) authentifizieren
  • sich mit den jeweiligen Geschäftsanwendungen und APIs verbinden
  • ihre Daten in ihrer eigenen isolierten Umgebung verarbeiten

Die Kombination schafft eine leistungsstarke Plattform für die Bereitstellung von Kundenlösungen, mit der Sie:

  • standardisierte Automatisierungsfunktionen schnell bereitstellen
  • Workflows je Kunde anpassen, ohne andere zu beeinflussen
  • Ihre Leistungen ohne lineare Kostensteigerungen ausweiten
  • vollständige Datensouveränität für jeden Kunden bewahren
  • SSO-basierte Zugriffskontrolle als Grundlage für Sicherheit und Compliance bieten

Dieser Ansatz baut auf denselben Prinzipien wie unser Leitfaden zur Einrichtung von Windmill mit Docker auf, erweitert sie jedoch zu einer vollständigen mandantenfähigen Architektur.

Erweiterte Konfigurationsoptionen

SSO auf Unternehmensniveau mit Authentik implementieren

Aktivieren Sie Single Sign-On über alle Kundendienste hinweg durch Konfiguration der Authentik-Forward-Authentifizierung:

# Add to the authentik-server labels in docker-compose.yml
- "traefik.http.middlewares.auth-global.forwardauth.address=http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
- "traefik.http.middlewares.auth-global.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.auth-global.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid"

Aktualisieren Sie anschließend die Labels Ihrer Dienste, damit sie die Middleware verwenden:

labels:
  - "traefik.http.routers.n8n.middlewares=auth-global"
  - "traefik.http.routers.nocodb.middlewares=auth-global"
  - "traefik.http.routers.supabase-studio.middlewares=auth-global"

Eine Vektordatenbank für erweiterte KI-Funktionen hinzufügen

Erweitern Sie die KI-Funktionen mit der Qdrant-Vektordatenbank:

qdrant:
  image: qdrant/qdrant:v1.18.3
  container_name: ${TENANT_PREFIX}-qdrant
  environment:
    QDRANT__SERVICE__HTTP_PORT: 6333
    QDRANT__SERVICE__GRPC_PORT: 6334
  volumes:
    - qdrant_data:/qdrant/storage
  networks:
    - tenant
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.qdrant.rule=Host(`vector.${CLIENT_DOMAIN}`)"
    - "traefik.http.routers.qdrant.tls.certresolver=letsencrypt"
    - "traefik.http.services.qdrant.loadbalancer.server.port=6333"
    - "traefik.http.routers.qdrant.middlewares=${AUTH_MIDDLEWARE}"
# Also add qdrant_data: under the top-level volumes:

Hybride KI-Architektur implementieren

Für optimale Leistung empfiehlt sich ein hybrider Ansatz, der containerisierte und native KI kombiniert:

# Install Ollama natively on host for GPU acceleration
brew install ollama

# Configure containers to use native Ollama
# In docker-compose.yml, services can access via host.docker.internal:11434
n8n:
  environment:
    - OLLAMA_HOST=host.docker.internal:11434

Unter macOS ist dies relevant, weil Docker Desktop keine GPUs in Containern unterstützt, während nativ ausgeführtes Ollama die GPU-Beschleunigung direkt nutzt. In unserem Setup ergab dies eine ungefähr 5-6-fache Leistungsverbesserung, während die anderen Dienste weiterhin in Containern liefen.

Monitoring- und Observability-Stack

Fügen Sie umfassendes Monitoring pro Kunde hinzu:

prometheus:
  image: prom/prometheus:v3.13.1
  container_name: ${TENANT_PREFIX}-prometheus
  volumes:
    - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
    - prometheus_data:/prometheus
  networks:
    - tenant
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.prometheus.rule=Host(`metrics.${CLIENT_DOMAIN}`)"
    - "traefik.http.routers.prometheus.tls.certresolver=letsencrypt"
    - "traefik.http.routers.prometheus.middlewares=${AUTH_MIDDLEWARE}"

grafana:
  image: grafana/grafana:13.1.1
  container_name: ${TENANT_PREFIX}-grafana
  environment:
    GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
    GF_USERS_ALLOW_SIGN_UP: "false"
  volumes:
    - grafana_data:/var/lib/grafana
    - ./monitoring/dashboards:/var/lib/grafana/dashboards
  networks:
    - tenant
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.grafana.rule=Host(`monitoring.${CLIENT_DOMAIN}`)"
    - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
    - "traefik.http.routers.grafana.middlewares=${AUTH_MIDDLEWARE}"
# Also add prometheus_data: and grafana_data: under the top-level volumes:

Häufige Probleme und Lösungen

"Service Unavailable" oder HTTP-502-Fehler

Ein solcher Fehler bedeutet in der Regel, dass Traefik den Zielcontainer nicht erreichen kann. Prüfen Sie:

# Verify container is running and healthy
docker compose ps
docker compose logs traefik --tail=20

# Check container is on correct network
docker network ls
docker network inspect ${CLIENT_PREFIX}-network

# Verify Traefik labels are correct
docker compose config --services

DNS-Auflösungsprobleme

Die Wildcard-DNS-Einrichtung ist entscheidend für das Subdomain-Routing:

# Correct Cloudflare DNS configuration
*.client-a.com CNAME tunnel-uuid.cfargotunnel.com
*.client-b.org CNAME tunnel-uuid2.cfargotunnel.com

# Test DNS resolution
nslookup workflows.client-a.com
dig workflows.client-a.com

Ressourcenengpässe bei mehreren Kunden

Überwachen Sie die Ressourcennutzung über alle Kundenumgebungen hinweg:

# Check overall system resource usage
docker stats --no-stream
htop

# Check disk usage per client
du -sh deployments/*/
df -h

# Monitor container memory usage
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}"

Ausgeschöpfter Datenbank-Verbindungspool

PostgreSQL-Verbindungslimits können bei vielen Kunden erreicht werden. Konfigurieren Sie pro Bereitstellung:

# Connect to client database
docker compose exec postgres psql -U postgres

-- Increase connection limit
ALTER SYSTEM SET max_connections = 300;
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET effective_cache_size = '1GB';

-- Apply: effective_cache_size reloads, max_connections and shared_buffers need a restart
SELECT pg_reload_conf();
\q

# Restart PostgreSQL
docker compose restart postgres

Authentik-SSO-Konfigurationsprobleme

Häufige SSO-Setup-Probleme und Lösungen:

# Check Authentik containers are running
docker compose ps | grep authentik

# Verify database initialization
docker compose exec postgres psql -U postgres -d authentik -c "\dt"

# Check Authentik logs for startup issues
docker compose logs authentik-server --tail=50

# Get a recovery link to log in as admin if needed
docker compose exec authentik-worker ak create_recovery_key 10 akadmin

Probleme mit der Cloudflare-Tunnel-Verbindung

Prüfen Sie Verbindungsprobleme des Tunnels:

# Check tunnel status
docker compose logs cloudflare-tunnel --tail=20

# Verify tunnel configuration in Cloudflare dashboard
# Ensure wildcard routing: *.client-domain.com

# Test tunnel connectivity
curl -I https://test.client-domain.com

Infrastrukturüberlegungen

Dimensionierung Ihrer Infrastruktur für mehrere Kunden

Für ein typisches Setup mit 10-15 gleichzeitigen Kunden mit vollständigen 16-Container-Stacks:

Mindestanforderungen:

  • CPU: 16-24 Kerne (2 Kerne pro aktiver Kundenumgebung)
  • RAM: 64-128 GB (4-8 GB pro Kunde je nach KI-Nutzung)
  • Speicher: NVMe SSD mit 2 TB+ (Datenbanken, KI-Modelle und Logs wachsen mit der Zeit)
  • Netzwerk: Gigabit-Verbindung für reaktionsschnellen Kundenzugriff

Empfohlen für den Produktivbetrieb:

  • Server: Hetzner CCX62 oder vergleichbar (48 vCPU, 192 GB RAM)
  • Speicher: 4 TB NVMe mit automatisiertem Backup-System
  • Netzwerk: Mehrere redundante Verbindungen
  • Monitoring: Vollständiger Observability-Stack mit Alarmierung

Backup-Strategie für mehrere Kundenumgebungen

Implementieren Sie automatisierte Backups pro Kunde:

#!/bin/bash
# backup-all-clients.sh - Comprehensive backup solution
set -euo pipefail
umask 077

BACKUP_DIR="/opt/backups"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
# Path to development-stack/deployments
DEPLOYMENTS_DIR="/opt/development-stack/deployments"
mkdir -p "$BACKUP_DIR"

for client_dir in "$DEPLOYMENTS_DIR"/*/; do
    CLIENT_DOMAIN=$(basename "$client_dir")
    # Volume names start with the compose project name (TENANT_PREFIX in .env)
    PROJECT=$(grep '^COMPOSE_PROJECT_NAME=' "$client_dir/.env" | cut -d= -f2)
    echo "📦 Backing up client: $CLIENT_DOMAIN"

    # Subshell: the cd applies to this client only; set -e stops the script on any failure
    (
        cd "$client_dir"

        # Backup databases with compression
        docker compose exec -T postgres pg_dumpall -U postgres | gzip > "${BACKUP_DIR}/${CLIENT_DOMAIN}_db_${BACKUP_DATE}.sql.gz"

        # Backup persistent volumes
        docker run --rm \
            -v "${BACKUP_DIR}":/backup \
            -v "${PROJECT}_n8n_data":/data/n8n:ro \
            -v "${PROJECT}_nocodb_data":/data/nocodb:ro \
            -v "${PROJECT}_ollama_data":/data/ollama:ro \
            alpine:3.22 tar czf "/backup/${CLIENT_DOMAIN}_volumes_${BACKUP_DATE}.tar.gz" -C /data .

        # Backup configuration files
        tar czf "${BACKUP_DIR}/${CLIENT_DOMAIN}_config_${BACKUP_DATE}.tar.gz" \
            docker-compose.yml .env traefik/ supabase/ authentik/ init/
    )

    echo "✅ Backup completed for $CLIENT_DOMAIN"
done

# Cleanup old backups (keep 30 days)
find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete

# Optional: Upload to cloud storage
# rclone sync "$BACKUP_DIR" s3:backup-bucket/multi-tenant/

Sicherheitshärtung für den Produktivbetrieb

Implementieren Sie umfassende Best Practices für die Sicherheit:

# Enhanced Traefik security configuration
traefik:
  command:
    - "--api.dashboard=true"
    - "--ping=true"
    - "--log.level=WARN"
    - "--accesslog=true"
    - "--providers.docker=true"
    - "--providers.docker.network=${TENANT_NETWORK}"
    - "--providers.docker.exposedbydefault=false"
    - "--providers.docker.constraints=Label(`com.docker.compose.project`,`${TENANT_PREFIX}`)"
    - "--entrypoints.web.address=:80"
    - "--entrypoints.websecure.address=:443"
    - "--entrypoints.websecure.asdefault=true"
    - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
    - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
    - "--entrypoints.traefik.address=:8080"
    - "--certificatesresolvers.letsencrypt.acme.email=${ADMIN_EMAIL}"
    - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
    - "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
  labels:
    # Security headers middleware
    - "traefik.http.middlewares.security.headers.customRequestHeaders.X-Forwarded-Proto=https"
    - "traefik.http.middlewares.security.headers.customResponseHeaders.X-Frame-Options=DENY"
    - "traefik.http.middlewares.security.headers.customResponseHeaders.X-Content-Type-Options=nosniff"
    - "traefik.http.middlewares.security.headers.customResponseHeaders.Strict-Transport-Security=max-age=31536000"
    - "traefik.http.middlewares.security.headers.customResponseHeaders.Content-Security-Policy=default-src 'self'"

    # Rate limiting middleware
    - "traefik.http.middlewares.ratelimit.ratelimit.burst=100"
    - "traefik.http.middlewares.ratelimit.ratelimit.average=50"

Wenden Sie die Sicherheits-Middleware auf alle Kundendienste an:

labels:
  # Without SSO
  - "traefik.http.routers.n8n.middlewares=security,ratelimit"
  # With SSO enabled (AUTH_MIDDLEWARE=auth-global)
  # - "traefik.http.routers.n8n.middlewares=security,ratelimit,auth-global"

Kostenanalyse: Die Zahlen, die zählen

Herkömmliche SaaS-Kosten (10 Unternehmenskunden mit vollem Funktionsumfang)

Monatliche Kosten pro Kunde:

  • n8n Pro: 50 $/Monat pro Kunde = 500 $/Monat
  • Supabase Pro: 25 $/Monat pro Kunde = 250 $/Monat
  • NoCode-Plattform (Airtable): 20 $/Monat pro Kunde = 200 $/Monat
  • SSO auf Unternehmensniveau (Auth0): 23 $/Monat pro Kunde = 230 $/Monat
  • KI-API-Kosten (OpenAI): 50 $/Monat pro Kunde = 500 $/Monat
  • Gesamt: 1.680 $/Monat = 20.160 $/Jahr

Kosten eines selbst betriebenen Stacks für mehrere Mandanten mit Unternehmensfunktionen

Jährliche Infrastrukturkosten:

  • Cloud-Server mit dedizierten vCPUs (Hetzner CCX62): 350 $/Monat = 4.200 $/Jahr
  • Domain-Kosten (10 Kunden): 120 $/Jahr
  • Cloudflare Pro (optional): 240 $/Jahr
  • Gesamt: 4.560 $/Jahr

Jährliche Einsparungen: 15.600 $ (77 % Kostenreduktion)

Diese Zahlen stammen aus der ursprünglichen Anleitung vom August 2025. Stand Oktober 2026 beginnt der B2C-Essentials-Tarif von Auth0 bei 35 $/Monat, und Hetzner führt den CCX62 nicht mehr auf; sein Nachfolger CCX63 mit 48 vCPUs und 192 GB kostet laut Preisliste 853,99 € pro Monat ohne Mehrwertsteuer, einschließlich einer primären IPv4-Adresse. Berechnen Sie beide Seiten vor Ihrer Entscheidung mit aktuellen Preisen neu.

Zusätzlich erhalten Sie:

  • Vollständige Datensouveränität und Datenschutz
  • Unbegrenzte Anpassung und White-Labeling
  • Keine Anbieterbindung oder Aufruflimits durch SaaS-Tarife
  • Sicherheits- und Compliance-Kontrollen, die Sie selbst verwalten
  • Möglichkeit, Reseller-Dienste anzubieten
  • Vollständige Kontrolle über Updates und Funktionen

Besonders deutlich wird der Vorteil dadurch, dass unser Setup Unternehmensfunktionen bereitstellt, die in SaaS-Abonnements typischerweise erheblich mehr kosten würden – ähnlich den Kostenvorteilen, die wir in unserer Analyse zum Self-Hosting von n8n gezeigt haben.

Von der Konfiguration zur Betriebsentscheidung

Die praktische Frage ist nicht, ob sich eine Plattform konfigurieren lässt. Entscheidend ist, ob das Team Verantwortlichkeiten erklären, Abweichungen erkennen, ohne Improvisation wiederherstellen und nachweisen kann, dass die Änderung das beabsichtigte Ergebnis verbessert hat. Deshalb verbinden wir jede Konfiguration mit einer verantwortlichen Person, einem Referenzzustand, einem Rücksetzpfad und einem Kontrollzeitraum nach der Änderung. So wird aus einer einmaligen Korrektur eine belastbare Fähigkeit im Betrieb. Die Dokumentation für den Produktivbetrieb sollte den Grund der Änderung, die geprüften Nachweise, den genauen Umfang und die Person enthalten, die zum Stoppen oder Rücksetzen befugt ist. Nach der Freigabe vergleichen wir die vereinbarten Indikatoren mit dem Referenzzustand und halten das Ergebnis in der Änderungsdokumentation fest. Bleibt die erwartete Verbesserung aus, ist das Rücksetzen ein gültiges Ergebnis und kein Scheitern. Diese Disziplin ist wichtiger als jeder einzelne Konfigurationswert. Dieselbe Dokumentation gibt der nächsten verantwortlichen Person einen verlässlichen Ausgangspunkt und macht spätere Optimierungen zu messbaren Entscheidungen statt zu einer neuen Runde von Vermutungen.

Weiterführende Beiträge

Weiterführende Beiträge