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.
- Reverse Proxies stapeln: Eine Architektur für den Produktivbetrieb
- Notfallwiederherstellung für selbst betriebene Dienste: Unsere Backup-Strategie
- Über hundert Docker-Container: Unsere monatliche Zustandsprüfung
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
- Mehrere Datenbankinstanzen auf einem Server selbst betreiben
- Ein mehrstufiges Sicherheits-Audit: Eine Next.js-Anwendung absichern
- Die Self-Hosting-Entscheidung: Wann SaaS mehr kostet als eigene Infrastruktur
- Hetzner Cloud Ende 2026: Tarife und Preise für das Self-Hosting von n8n, Windmill, Twenty und Ollama