Construire une stack de développement multi-tenant avec Docker : configuration complète pour des déploiements clients évolutifs
Comment créer un environnement de développement multi-tenant basé sur des modèles, avec 16 services conteneurisés, fonctionnant hors ligne tout en restant accessible en ligne grâce au routage par sous-domaines
La gestion des environnements de développement pour plusieurs clients implique souvent de choisir entre des configurations manuelles complexes ou des solutions cloud coûteuses. Les déploiements manuels sont chronophages et sujets aux erreurs. Les plateformes cloud sont pratiques mais créent une dépendance vis-à-vis du fournisseur et des coûts récurrents qui augmentent avec l'utilisation.
Aujourd'hui, nous allons parcourir la construction d'une stack de développement multi-tenant évolutive qui vous offre les deux : une isolation complète entre les environnements clients avec des capacités de déploiement automatisé, tout en conservant un contrôle total sur votre infrastructure. Cette approche s'inscrit dans notre philosophie de solutions auto-hébergées — similaire à ce que nous avons montré avec l'auto-hébergement de n8n pour l'automatisation des workflows et le déploiement de Windmill avec Docker pour un contrôle opérationnel complet.
Les outils que nous utilisons
Commençons par comprendre le rôle de chaque composant dans notre architecture complète à 16 conteneurs :
Docker : votre fondation de conteneurisation
Docker fournit l'isolation et la cohérence dont nous avons besoin pour les environnements multi-tenant. Chaque client dispose de ses propres conteneurs avec des configurations identiques, garantissant que ce qui fonctionne en développement fonctionnera en production. Pensez-y comme si vous aviez plusieurs serveurs complètement séparés fonctionnant sur le même matériel.
L'avantage principal ? Une isolation parfaite entre les clients. Les données, configurations et personnalisations d'un client n'interfèrent jamais avec celles d'un autre. C'est crucial lorsqu'on gère plusieurs clients professionnels ayant des exigences et des besoins de sécurité différents.
Traefik : proxy inverse intelligent et répartiteur de charge
Traefik agit comme un directeur de trafic intelligent, routant automatiquement les requêtes vers le bon environnement client en fonction des noms de domaine. Au lieu de configurer manuellement des règles Apache ou Nginx complexes, Traefik lit les labels de vos conteneurs Docker et configure le routage automatiquement.
Pensez à Traefik comme un réceptionniste intelligent qui sait exactement vers quel bureau (conteneur) chaque visiteur (requête) doit être dirigé, sans que vous ayez à donner des indications à chaque fois. Dans notre configuration, Traefik gère la terminaison SSL, la découverte automatique de services et fournit des tableaux de bord de surveillance détaillés.
Cloudflare Tunnels : accès externe sécurisé
Les Cloudflare Tunnels fournissent un accès sécurisé à votre stack de développement locale sans configurations complexes de pare-feu ou de VPN. Chaque domaine client dispose de son propre tunnel, assurant une séparation complète au niveau réseau tout en maintenant une sécurité de niveau entreprise.
L'avantage est que vos environnements de développement restent locaux et sécurisés, mais les clients peuvent accéder à leurs services spécifiques depuis n'importe où avec une authentification appropriée — similaire à la façon dont nous avons configuré l'accès externe sécurisé dans notre guide d'hébergement n8n.
La stack de services complète : tout ce dont vos clients ont besoin
Notre stack multi-tenant comprend sept catégories de services de base réparties sur 16 conteneurs par client :
Automatisation des workflows et logique métier :
- n8n : plateforme complète d'automatisation des workflows pour l'automatisation des processus métier
- Authentik : gestion de l'authentification unique (SSO) et des identités de niveau entreprise (3 conteneurs : serveur, worker, cache Redis)
Base de données et services backend :
- PostgreSQL : backend de base de données robuste supportant tous les services avec pooling de connexions optimisé
- Stack Supabase : backend-as-a-service complet avec 5 conteneurs spécialisés (Studio, Auth, API REST, Realtime, Kong Gateway)
- NocoDB : interface de base de données no-code pour la gestion des données clients
IA et intelligence :
- Ollama : modèles de langage IA locaux avec accélération GPU pour l'automatisation intelligente
- Qdrant (optionnel) : base de données vectorielle pour les workflows IA avancés et la recherche par similarité
Infrastructure et surveillance :
- Cloudflare Tunnel : connectivité externe sécurisée
- Traefik : proxy inverse avec SSL automatique et tableau de bord de surveillance
Comment tout fonctionne ensemble
Voici le flux complet lorsqu'un client accède à son environnement :
- Le client navigue vers son domaine personnalisé (par ex.,
workflows.client-a.com) - Le Cloudflare Tunnel route la requête vers votre instance Traefik locale
- Traefik lit le domaine, applique les middlewares (authentification, SSL, limitation de débit) et transmet au bon conteneur client
- Authentik gère l'authentification SSO sur tous les services si configuré
- Le client obtient son environnement complètement isolé avec ses données et configurations
- Tous les autres clients restent complètement non affectés et inaccessibles
Tout reste organisé et séparé, chaque client disposant de sa propre structure de sous-domaines comme auth.client-a.com, database.client-a.com, backend.client-a.com, etc.
Mise en place : les étapes pratiques
Préparer les fondations
Tout d'abord, vous aurez besoin de Docker Desktop installé et d'une configuration de gestion de domaines. Nous recommandons la mise en place d'une structure DNS avec caractères génériques (wildcard) pour faciliter l'intégration des clients :
# 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
Créer le système de modèles
La magie opère grâce à une approche basée sur des modèles. Au lieu de configurer manuellement chaque client, nous créons des modèles qui peuvent être déployés instantanément avec des configurations spécifiques au client.
Créez la structure de répertoires complète :
mkdir -p development-stack/{template,deployments}
cd development-stack/template
# Create service-specific configuration directories
mkdir -p {traefik,authentik,supabase,init}
Configuration complète du modèle multi-services
Créez un modèle complet de docker-compose.yml avec les 16 services :
version: '3.8'
networks:
${TENANT_NETWORK}:
driver: bridge
services:
# External Connectivity
cloudflare-tunnel:
image: cloudflare/cloudflared:latest
container_name: ${TENANT_PREFIX}-tunnel
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TOKEN}
networks:
- ${TENANT_NETWORK}
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "cloudflared tunnel info ${TUNNEL_ID} || exit 1"]
interval: 30s
timeout: 10s
retries: 3
# Reverse Proxy & Load Balancer
traefik:
image: traefik:v3.0
container_name: ${TENANT_PREFIX}-traefik
command:
- "--api.dashboard=true"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.network=${TENANT_NETWORK}"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=${ADMIN_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
ports:
- "${TRAEFIK_PORT}:80"
- "${TRAEFIK_SECURE_PORT}:443"
- "${TRAEFIK_DASHBOARD_PORT}:8080"
networks:
- ${TENANT_NETWORK}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/acme.json:/acme.json
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.${CLIENT_DOMAIN}`)"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
healthcheck:
test: ["CMD", "traefik", "healthcheck"]
interval: 30s
timeout: 10s
retries: 3
# Database Backend
postgres:
image: postgres:15-alpine
container_name: ${TENANT_PREFIX}-postgres
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_MULTIPLE_DATABASES: n8n,supabase,authentik,nocodb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d
networks:
- ${TENANT_NETWORK}
ports:
- "${POSTGRES_PORT}:5432"
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:latest
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_NETWORK}
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}"
depends_on:
postgres:
condition: service_healthy
# No-Code Database Interface
nocodb:
image: nocodb/nocodb:latest
container_name: ${TENANT_PREFIX}-nocodb
environment:
NC_DB: "pg://postgres:${POSTGRES_PASSWORD}@postgres:5432/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_NETWORK}
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}"
depends_on:
postgres:
condition: service_healthy
# Supabase Backend Stack (5 containers)
supabase-studio:
image: supabase/studio:latest
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_NETWORK}
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}"
healthcheck:
disable: true
depends_on:
postgres:
condition: service_healthy
supabase-meta:
image: supabase/postgres-meta:latest
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_NETWORK}
depends_on:
postgres:
condition: service_healthy
supabase-auth:
image: supabase/gotrue:latest
container_name: ${TENANT_PREFIX}-supabase-auth
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
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_NETWORK}
depends_on:
postgres:
condition: service_healthy
supabase-rest:
image: postgrest/postgrest:latest
container_name: ${TENANT_PREFIX}-supabase-rest
environment:
PGRST_DB_URI: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/supabase
PGRST_DB_SCHEMAS: public,graphql_public
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
networks:
- ${TENANT_NETWORK}
depends_on:
postgres:
condition: service_healthy
supabase-realtime:
image: supabase/realtime:latest
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}
FLY_ALLOC_ID: fly123
FLY_APP_NAME: realtime
SECRET_KEY_BASE: ${SUPABASE_JWT_SECRET}
ERL_AFLAGS: -proto_dist inet_tcp
ENABLE_TAILSCALE: "false"
DNS_NODES: "''"
networks:
- ${TENANT_NETWORK}
command: >
sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
depends_on:
postgres:
condition: service_healthy
supabase-kong:
image: kong:3.2-alpine
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_NETWORK}
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}"
# Local AI Language Models
ollama:
image: ollama/ollama:latest
container_name: ${TENANT_PREFIX}-ollama
environment:
OLLAMA_HOST: 0.0.0.0:11434
OLLAMA_ORIGINS: "*"
volumes:
- ollama_data:/root/.ollama
networks:
- ${TENANT_NETWORK}
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}"
deploy:
resources:
limits:
memory: 16G
reservations:
memory: 8G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 3
# Enterprise SSO Authentication (3 containers)
authentik-redis:
image: redis:alpine
container_name: ${TENANT_PREFIX}-authentik-redis
command: --save 60 1 --loglevel warning
networks:
- ${TENANT_NETWORK}
volumes:
- authentik_redis_data:/data
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_NETWORK}
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"
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_NETWORK}
depends_on:
postgres:
condition: service_healthy
authentik-redis:
condition: service_healthy
# Test Service for Health Monitoring
whoami:
image: traefik/whoami:latest
container_name: ${TENANT_PREFIX}-whoami
networks:
- ${TENANT_NETWORK}
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"
volumes:
postgres_data:
n8n_data:
nocodb_data:
ollama_data:
authentik_redis_data:
authentik_media:
authentik_templates:
Modèle d'environnement complet
Créez le fichier .env.template pour les variables spécifiques à chaque client :
# Client Configuration
CLIENT_NAME=CLIENT_NAME_PLACEHOLDER
CLIENT_DOMAIN=CLIENT_DOMAIN_PLACEHOLDER
TENANT_PREFIX=CLIENT_PREFIX_PLACEHOLDER
TENANT_NETWORK=CLIENT_NETWORK_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
# Infrastructure Ports
TRAEFIK_PORT=80
TRAEFIK_SECURE_PORT=443
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=2024.8.3
# n8n Configuration
N8N_PROTOCOL=https
N8N_SECURE_COOKIE=true
# Cloudflare Integration
CLOUDFLARE_TOKEN=CLOUDFLARE_TOKEN_PLACEHOLDER
TUNNEL_ID=TUNNEL_ID_PLACEHOLDER
# Authentication Middleware (set to 'auth-global' for SSO, leave empty for no auth)
AUTH_MIDDLEWARE=
Scripts d'initialisation de la base de données
Créez l'initialisation complète de la base de données dans init/01-create-multiple-databases.sql :
-- Create databases for all services
CREATE DATABASE n8n;
CREATE DATABASE nocodb;
CREATE DATABASE supabase;
CREATE DATABASE authentik;
-- Grant permissions
GRANT ALL PRIVILEGES ON DATABASE n8n TO postgres;
GRANT ALL PRIVILEGES ON DATABASE nocodb TO postgres;
GRANT ALL PRIVILEGES ON DATABASE supabase TO postgres;
GRANT ALL PRIVILEGES ON DATABASE authentik TO postgres;
-- Enable required extensions for Supabase
\c supabase;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "pgjwt";
-- 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';
Configuration de la passerelle Kong pour Supabase
Créez le fichier supabase/kong.yml pour le routage de la passerelle API :
_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
Script de déploiement automatisé
Le script de déploiement complet qui crée de nouveaux environnements clients en quelques minutes :
#!/bin/bash
# deploy-client.sh - Complete Multi-Tenant Deployment
CLIENT_DOMAIN=$1
CLIENT_NAME=$2
CLOUDFLARE_TOKEN=$3
if [ -z "$CLIENT_DOMAIN" ] || [ -z "$CLIENT_NAME" ] || [ -z "$CLOUDFLARE_TOKEN" ]; then
echo "Usage: ./deploy-client.sh example.com 'Client Name' 'cloudflare-token'"
echo ""
echo "Example: ./deploy-client.sh client-a.com 'Client A Corporation' 'your-cloudflare-token'"
exit 1
fi
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/" 2>/dev/null || true
echo "Copied configuration templates"
# Generate secure passwords and secrets
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25)
NOCODB_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16)
SUPABASE_JWT_SECRET=$(openssl rand -base64 64 | tr -d "=+/" | cut -c1-64)
AUTHENTIK_SECRET=$(openssl rand -hex 32)
echo "Generated secure credentials"
# Calculate unique ports to avoid conflicts
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
cat .env.template | \
sed "s/CLIENT_NAME_PLACEHOLDER/$CLIENT_NAME/g" | \
sed "s/CLIENT_DOMAIN_PLACEHOLDER/$CLIENT_DOMAIN/g" | \
sed "s/CLIENT_PREFIX_PLACEHOLDER/$CLIENT_PREFIX/g" | \
sed "s/CLIENT_NETWORK_PLACEHOLDER/${CLIENT_PREFIX}-network/g" | \
sed "s/SECURE_PASSWORD_PLACEHOLDER/$POSTGRES_PASSWORD/g" | \
sed "s/NOCODB_PASSWORD_PLACEHOLDER/$NOCODB_PASSWORD/g" | \
sed "s/SUPABASE_JWT_PLACEHOLDER/$SUPABASE_JWT_SECRET/g" | \
sed "s/AUTHENTIK_SECRET_PLACEHOLDER/$AUTHENTIK_SECRET/g" | \
sed "s/CLOUDFLARE_TOKEN_PLACEHOLDER/$CLOUDFLARE_TOKEN/g" | \
sed "s/8080/$TRAEFIK_DASHBOARD_PORT/g" | \
sed "s/5432/$POSTGRES_PORT/g" \
> "$DEPLOY_DIR/.env"
echo "Generated environment configuration"
# Create ACE 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 core infrastructure first
docker-compose up -d cloudflare-tunnel traefik postgres
echo "Waiting for database to be ready..."
sleep 30
# Start all remaining services
docker-compose up -d
echo ""
echo "Multi-tenant environment deployed successfully!"
echo ""
echo "Access URLs:"
echo " Traefik Dashboard: http://localhost:$TRAEFIK_DASHBOARD_PORT"
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 (save these securely):"
echo " Client: $CLIENT_NAME"
echo " PostgreSQL Password: $POSTGRES_PASSWORD"
echo " NocoDB Admin Password: $NOCODB_PASSWORD"
echo " Supabase JWT Secret: [hidden - check .env file]"
echo ""
echo "Next Steps:"
echo " 1. Configure Cloudflare DNS: *.${CLIENT_DOMAIN} -> tunnel"
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"
Utiliser votre stack multi-tenant
Déployer de nouveaux clients
La création d'un nouvel environnement client devient triviale avec notre script complet :
# 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"
Chaque déploiement crée :
- Un réseau Docker complètement isolé avec 16 conteneurs
- Des volumes de données séparés pour le stockage persistant
- Des conteneurs de services uniques avec surveillance de l'état de santé
- Une configuration individuelle de tunnel Cloudflare
- Un routage de domaine personnalisé avec certificats SSL
- Une infrastructure SSO de niveau entreprise prête à l'activation
Gestion de plusieurs environnements
Surveillez tous les environnements clients depuis un emplacement central :
# 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 latest images
docker-compose pull && docker-compose up -d
Analyse approfondie de l'architecture des conteneurs
Notre architecture complète de 16 conteneurs par client comprend :
Couche infrastructure (4 conteneurs) :
cloudflare-tunnel: connectivité externe sécuriséetraefik: proxy inverse avec SSL automatique et découverte de servicespostgres: base de données centrale avec pooling de connexionswhoami: surveillance de l'état de santé et vérification du routage
Couche application (7 conteneurs) :
n8n: automatisation des workflows avec backend PostgreSQLnocodb: interface de base de données no-codesupabase-studio: tableau de bord de développement backendsupabase-meta: service d'introspection de la base de donnéessupabase-auth: authentification et gestion des utilisateurssupabase-rest: API REST auto-généréesupabase-realtime: abonnements et mises à jour en temps réel
Couche IA et passerelle (2 conteneurs) :
ollama: IA locale avec support d'accélération GPUsupabase-kong: passerelle API avec limitation de débit et CORS
Couche sécurité entreprise (3 conteneurs) :
authentik-server: serveur d'authentification SSOauthentik-worker: tâches en arrière-plan et notificationsauthentik-redis: gestion des sessions et mise en cache
Dimensionnement des ressources par client
Ajustez les ressources en fonction des besoins et des modèles d'utilisation de chaque client :
# High-performance client configuration
services:
n8n:
deploy:
resources:
limits:
cpus: '4.0'
memory: 8G
reservations:
cpus: '2.0'
memory: 4G
postgres:
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 2G
environment:
- POSTGRES_MAX_CONNECTIONS=200
- POSTGRES_SHARED_BUFFERS=1GB
- POSTGRES_EFFECTIVE_CACHE_SIZE=3GB
ollama:
deploy:
resources:
limits:
memory: 32G
reservations:
memory: 16G
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Des avantages concrets pour votre entreprise
Isolation complète des clients avec fonctionnalités entreprise
Chaque client dispose de son propre univers complet incluant un SSO de niveau entreprise, des capacités d'IA et une infrastructure backend complète. Les données, configurations, personnalisations et politiques de sécurité restent complètement contenues. Un problème chez un client n'affecte jamais les autres, similaire à l'isolation que nous obtenons avec nos déploiements n8n individuels.
Intégration rapide des clients avec ensemble complet de fonctionnalités
Les nouveaux clients peuvent être opérationnels avec une stack complète de développement et d'automatisation en moins de 10 minutes. Le script de déploiement gère automatiquement toute la configuration complexe, la mise en place du DNS, l'initialisation des services et la configuration de la sécurité — bien plus complet que les approches traditionnelles.
Des coûts prévisibles de niveau entreprise
Après la configuration initiale, il n'y a aucun coût d'hébergement par client au-delà de votre infrastructure de base. Contrairement aux solutions SaaS qui facturent par utilisateur, par workflow ou par appel API, vous payez une fois pour le matériel et exécutez un nombre illimité d'environnements clients avec toutes les fonctionnalités entreprise.
Cohérence professionnelle de la marque
Chaque client dispose de ses propres domaines personnalisés avec des sous-domaines professionnels (workflows.client.com, auth.client.com, etc.) et peut personnaliser entièrement son environnement. Pas de mentions « powered by » ou d'interfaces partagées qui diluent l'identité de la marque.
L'intégration n8n : automatisation des workflows d'entreprise à grande échelle
C'est ici que les choses deviennent vraiment puissantes. Tout comme nous vous avons montré comment auto-héberger n8n pour l'automatisation des workflows, cette configuration multi-tenant donne à chaque client sa propre instance n8n complète intégrée à une stack entreprise complète.
Chaque client peut construire des workflows sophistiqués qui :
- Se connectent à leurs propres bases de données (NocoDB, Supabase PostgreSQL)
- Utilisent leurs propres modèles d'IA (Ollama) pour l'automatisation intelligente
- S'authentifient via le SSO d'entreprise (Authentik)
- S'intègrent avec leurs outils métier et API spécifiques
- Traitent leurs données avec une isolation et une sécurité complètes
Cette combinaison crée une puissante plateforme de livraison client où vous pouvez :
- Déployer rapidement des capacités d'automatisation standardisées
- Personnaliser les workflows par client sans affecter les autres
- Développer votre offre de services sans augmentation linéaire des coûts
- Maintenir une souveraineté totale des données pour chaque client
- Offrir une sécurité et une conformité de niveau entreprise
Cette approche s'appuie sur les mêmes principes que ceux utilisés dans notre guide de configuration Docker de Windmill, mais l'étend à une architecture multi-tenant complète.
Options de configuration avancées
Mise en place du SSO d'entreprise avec Authentik
Activez l'authentification unique sur tous les services client en configurant l'authentification par transfert Authentik :
# Add to Traefik middleware configuration
middlewares:
auth-global:
forwardAuth:
address: "http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
trustForwardHeader: true
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
Puis mettez à jour les labels de vos services pour utiliser le middleware :
labels:
- "traefik.http.routers.n8n.middlewares=auth-global"
- "traefik.http.routers.nocodb.middlewares=auth-global"
- "traefik.http.routers.supabase-studio.middlewares=auth-global"
Ajout d'une base de données vectorielle pour l'IA avancée
Améliorez les capacités d'IA avec la base de données vectorielle Qdrant :
qdrant:
image: qdrant/qdrant:latest
container_name: ${TENANT_PREFIX}-qdrant
environment:
QDRANT__SERVICE__HTTP_PORT: 6333
QDRANT__SERVICE__GRPC_PORT: 6334
volumes:
- qdrant_data:/qdrant/storage
networks:
- ${TENANT_NETWORK}
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}"
Mise en place d'une architecture IA hybride
Pour des performances optimales, envisagez une approche hybride combinant IA conteneurisée et native :
# 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
Cela offre une amélioration des performances de 5 à 6 fois grâce à l'accès direct au GPU, tout en maintenant l'isolation des conteneurs pour les autres services.
Stack de surveillance et d'observabilité
Ajoutez une surveillance complète par client :
prometheus:
image: prom/prometheus:latest
container_name: ${TENANT_PREFIX}-prometheus
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
networks:
- ${TENANT_NETWORK}
labels:
- "traefik.enable=true"
- "traefik.http.routers.prometheus.rule=Host(`metrics.${CLIENT_DOMAIN}`)"
grafana:
image: grafana/grafana:latest
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_NETWORK}
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`monitoring.${CLIENT_DOMAIN}`)"
- "traefik.http.routers.grafana.middlewares=${AUTH_MIDDLEWARE}"
Problèmes courants et solutions
Erreurs « Service Unavailable » ou HTTP 502
Cela signifie généralement que Traefik ne peut pas atteindre le conteneur cible. Vérifiez que :
# 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
Problèmes de résolution DNS
La configuration DNS wildcard est cruciale pour le routage par sous-domaines :
# 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
Épuisement des ressources sur plusieurs clients
Surveillez l'utilisation des ressources sur tous les environnements clients :
# 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-compose -f deployments/*/docker-compose.yml ps --format "table {{.Name}}\t{{.Size}}"
Épuisement du pool de connexions à la base de données
Les limites de connexions PostgreSQL peuvent être atteintes avec de nombreux clients. Configurez par déploiement :
-- 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';
-- Reload configuration
SELECT pg_reload_conf();
Problèmes de configuration du SSO Authentik
Problèmes courants de configuration SSO et solutions :
# 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
# Reset Authentik admin password if needed
docker-compose exec authentik-server ak create_admin_group
docker-compose exec authentik-server ak bootstrap_tasks
Problèmes de connexion du tunnel Cloudflare
Déboguer les problèmes de connectivité du tunnel :
# 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
Considérations d'infrastructure
Dimensionner votre infrastructure pour plusieurs clients
Pour une configuration typique gérant 10 à 15 clients simultanément avec des stacks complètes de 16 conteneurs :
Exigences minimales :
- CPU : 16-24 cœurs (2 cœurs par environnement client actif)
- RAM : 64-128 Go (4-8 Go par client selon l'utilisation de l'IA)
- Stockage : SSD NVMe avec 2 To+ (les bases de données, modèles d'IA et journaux augmentent avec le temps)
- Réseau : connexion Gigabit pour un accès client réactif
Recommandé pour la production :
- Serveur : Hetzner CCX62 ou similaire (48 vCPU, 192 Go de RAM)
- Stockage : 4 To NVMe avec système de sauvegarde automatisé
- Réseau : connexions redondantes multiples
- Surveillance : stack d'observabilité complète avec alertes
Stratégie de sauvegarde pour les environnements multi-clients
Mettez en place des sauvegardes automatisées par client :
#!/bin/bash
# backup-all-clients.sh - Comprehensive backup solution
BACKUP_DIR="/opt/backups"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
for client_dir in deployments/*/; do
if [ -d "$client_dir" ]; then
CLIENT_DOMAIN=$(basename "$client_dir")
echo "Backing up client: $CLIENT_DOMAIN"
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 "${PWD}":/backup \
-v "${CLIENT_DOMAIN//.}_n8n_data":/data/n8n:ro \
-v "${CLIENT_DOMAIN//.}_nocodb_data":/data/nocodb:ro \
-v "${CLIENT_DOMAIN//.}_ollama_data":/data/ollama:ro \
alpine tar czf "/backup/${BACKUP_DIR}/${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/
echo "Backup completed for $CLIENT_DOMAIN"
fi
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/
Renforcement de la sécurité pour la production
Mettez en œuvre les meilleures pratiques de sécurité complètes :
# Enhanced Traefik security configuration
traefik:
command:
- "--api.dashboard=true"
- "--api.debug=false"
- "--log.level=WARN"
- "--accesslog=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--providers.docker.exposedbydefault=false"
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"
Appliquez le middleware de sécurité à tous les services clients :
labels:
- "traefik.http.routers.n8n.middlewares=security,ratelimit,${AUTH_MIDDLEWARE}"
Analyse des coûts : les chiffres qui comptent
Coûts SaaS traditionnels (10 clients entreprise avec ensemble complet de fonctionnalités)
Coûts mensuels par client :
- n8n Pro : 50 $/mois par client = 500 $/mois
- Supabase Pro : 25 $/mois par client = 250 $/mois
- Plateforme NoCode (Airtable) : 20 $/mois par client = 200 $/mois
- SSO entreprise (Auth0) : 23 $/mois par client = 230 $/mois
- Coûts API IA (OpenAI) : 50 $/mois par client = 500 $/mois
- Total : 1 500 $/mois = 18 000 $/an
Coûts de la stack entreprise multi-tenant auto-hébergée
Coûts d'infrastructure annuels :
- Serveur dédié (Hetzner CCX62) : 350 $/mois = 4 200 $/an
- Coûts des domaines (10 clients) : 120 $/an
- Cloudflare Pro (optionnel) : 240 $/an
- Total : 4 560 $/an
Économies annuelles : 13 440 $ (réduction des coûts de 75 %)
De plus, vous bénéficiez de :
- Souveraineté et confidentialité totales des données
- Personnalisation et marque blanche illimitées
- Aucune dépendance vis-à-vis d'un fournisseur ni limitation de débit API
- Sécurité et conformité de niveau entreprise
- Possibilité de proposer des services de revente
- Contrôle total sur les mises à jour et les fonctionnalités