tva
← Analyses

Docker multi-tenant sans risque de panne commune involontaire

Un article de suivi à jour, axé sur la production, qui transforme les recommandations des éditeurs en contrôles opérationnels, décisions de migration et critères de mise en service vérifiables.

Ce qui a changé en 2026

Nous revenons sur ce sujet parce que les limites opérationnelles ont évolué. Les principes durables restent pertinents, mais les versions actuelles rendent certains anciens raccourcis incomplets ou risqués. Ce suivi part de la documentation des éditeurs disponible au 14 juillet 2026, distingue les faits des choix locaux et traite chaque modification de configuration comme une intervention contrôlée en production.

Sources primaires actuelles

Comment exploiter cette mise à jour

Lisez d’abord les sources, consignez les versions réellement déployées et définissez le résultat observable avant de modifier un paramètre. Testez la plus petite intervention réversible dans un environnement représentatif. Une commande réussie ne constitue pas le critère d’acceptation : l’état du service, l’intégrité des données, la latence, les limites de sécurité et le délai de retour à la version précédente le sont. Lorsque l’ancien guide propose une séquence de diagnostic utile, nous la conservons ci-dessous comme socle opérationnel ; les exemples propres à une version doivent toutefois être vérifiés dans la documentation actuelle.

Socle opérationnel

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. Les fichiers de configuration, scripts et exemples de commandes ci-dessous sont repris du guide d’origine et corrigés là où c’était nécessaire.

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 voir comment construire une pile logicielle de développement multi-tenant évolutive qui vous offre les deux : une isolation forte 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 — comme nous l’avons montré avec l’auto-hébergement de n8n pour l’automatisation des flux de travail 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, ce qui contribue à ce que ce qui fonctionne en développement fonctionne aussi en production. C’est comme disposer de plusieurs serveurs séparés sur le même matériel, bien qu’ils partagent toujours le processeur, la mémoire, le disque et le noyau.

L’avantage principal ? Une isolation forte entre les clients. Les données, configurations et personnalisations de chaque client résident dans des conteneurs, réseaux et volumes séparés, de sorte qu’elles n’interfèrent pas avec celles des autres. 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 aiguilleur 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 étiquettes 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 supervision détaillés.

Cloudflare Tunnels : accès externe sécurisé

Les Cloudflare Tunnels fournissent un accès sécurisé à votre pile logicielle de développement locale sans configurations complexes de pare-feu ou de VPN. Chaque domaine client dispose de son propre tunnel, ce qui maintient séparés les chemins d’accès externe de chaque client.

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 — comme nous avons configuré l’accès externe sécurisé dans notre guide d’hébergement n8n.

La pile complète de services : tout ce dont vos clients ont besoin

Notre pile logicielle multi-tenant comprend quatre catégories de services de base réparties sur 16 conteneurs par client :

Automatisation des flux de travail et logique métier :

  • n8n : plateforme complète d’automatisation des flux de travail 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)

Bases de données et services backend :

  • PostgreSQL : backend de base de données robuste prenant en charge tous les services avec un pool de connexions optimisé
  • Pile Supabase : backend en tant que service complet avec 6 conteneurs spécialisés (Studio, Meta, Auth, API REST, Realtime, Kong Gateway)
  • NocoDB : interface de base de données sans 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 flux de travail IA avancés et la recherche par similarité

Infrastructure et supervision :

  • Cloudflare Tunnel : connectivité externe sécurisée
  • Traefik : proxy inverse avec SSL automatique et tableau de bord de supervision

Comment tout fonctionne ensemble

Voici le flux complet lorsqu’un client accède à son environnement :

  • Le client navigue vers son domaine personnalisé (par exemple, 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 propre environnement isolé avec ses données et configurations
  • Les environnements des autres clients ne sont pas accessibles par les routes de ce client

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

Le principe repose 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 propres à chaque 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 :

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:

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
# 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=

Scripts d’initialisation de la base de données

Créez un script complet d’initialisation 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;

-- 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'

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
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"

Utiliser votre pile logicielle multi-tenant

Déployer de nouveaux clients

La création d’un nouvel environnement client devient triviale avec notre script complet :

# 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"

Chaque déploiement crée :

  • Un réseau Docker dédié avec 16 conteneurs
  • Des volumes de données séparés pour le stockage persistant
  • Des conteneurs de services uniques avec supervision de l’état des services
  • 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 the image versions now pinned in docker-compose.yml
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ée
  • traefik : proxy inverse avec SSL automatique et découverte de services
  • postgres : base de données centrale avec un pool de connexions
  • whoami : supervision de l’état des services et vérification du routage

Couche application (7 conteneurs) :

  • n8n : automatisation des flux de travail avec backend PostgreSQL
  • nocodb : interface de base de données sans code
  • supabase-studio : tableau de bord de développement backend
  • supabase-meta : service d’introspection de la base de données
  • supabase-auth : authentification et gestion des utilisateurs
  • supabase-rest : API REST auto-générée
  • supabase-realtime : abonnements et mises à jour en temps réel

Couche IA et passerelle (2 conteneurs) :

  • ollama : IA locale avec prise en charge de l’accélération GPU
  • supabase-kong : passerelle API avec limitation de débit et CORS

Couche sécurité entreprise (3 conteneurs) :

  • authentik-server : serveur d’authentification SSO
  • authentik-worker : tâches en arrière-plan et notifications
  • authentik-redis : gestion des sessions et mise en cache

Dimensionnement des ressources par client

Ajustez les ressources en fonction des besoins et des profils d’utilisation de chaque client :

# 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]

Des avantages concrets pour votre entreprise

Isolation complète des clients avec fonctionnalités pour l’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 séparées. Un problème au niveau applicatif dans l’environnement d’un client reste circonscrit à cet environnement. Toutefois, comme tous les clients partagent le même hôte, des problèmes au niveau de l’hôte, comme la saturation du processeur, de la mémoire ou du disque, peuvent encore tous les affecter. Cela ressemble à l’isolation de 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 pile logicielle complète de développement et d’automatisation en moins de 10 minutes. Le script de déploiement automatise la configuration, la génération des identifiants et l’initialisation des services ; les enregistrements DNS Cloudflare et le SSO facultatif restent de courtes étapes manuelles.

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 flux de travail ou par appel API, vous payez un montant fixe pour le serveur et pouvez exécuter autant d’environnements clients avec toutes les fonctionnalités pour l’entreprise que ses ressources le permettent.

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 flux de travail 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 flux de travail, cette configuration multi-tenant donne à chaque client sa propre instance n8n complète intégrée à une pile logicielle complète pour l’entreprise.

Chaque client peut construire des flux de travail 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 dans leur propre environnement isolé

Cette combinaison crée une puissante plateforme de services aux clients où vous pouvez :

  • Déployer rapidement des capacités d’automatisation standardisées
  • Personnaliser les flux de travail 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 un contrôle d’accès basé sur le SSO comme fondement du travail de sécurité et de conformité

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 déléguée à Authentik :

# 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"

Puis mettez à jour les étiquettes 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: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:

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

Sur macOS, cela compte car Docker Desktop ne prend pas en charge les GPU dans les conteneurs, tandis qu’Ollama exécuté nativement utilise directement l’accélération GPU. Dans notre configuration, les performances ont été multipliées par environ 5 à 6, tandis que les autres services sont restés conteneurisés.

Pile de supervision et d’observabilité

Ajoutez une supervision complète par client :

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:

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 à caractères génériques 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 stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}"

É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';

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

# Restart PostgreSQL
docker compose restart postgres

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

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

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 piles logicielles 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
  • Supervision : pile logicielle 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
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/

Renforcement de la sécurité pour la production

Mettez en œuvre un ensemble complet de bonnes pratiques de sécurité :

# 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"

Appliquez le middleware de sécurité à tous les services clients :

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"

Analyse des coûts : les chiffres qui comptent

Coûts SaaS traditionnels (10 clients professionnels 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 sans code (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 680 $/mois = 20 160 $/an

Coûts de la pile logicielle multi-tenant auto-hébergée pour l’entreprise

Coûts d’infrastructure annuels :

  • Serveur cloud avec vCPU dédiés (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 : 15 600 $ (réduction des coûts de 77 %)

Ces chiffres proviennent du guide original d’août 2025. En octobre 2026, le forfait B2C Essentials d’Auth0 commence à 35 $/mois et Hetzner ne propose plus le CCX62 dans son catalogue ; son successeur, le CCX63, doté de 48 vCPU et de 192 GB de mémoire, est affiché à 853,99 €/mois hors taxe sur la valeur ajoutée, avec une adresse IPv4 principale incluse. Recalculez les deux options avec les prix actuels avant de vous décider.

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 limite de débit imposée par les forfaits SaaS
  • Des contrôles de sécurité et de conformité que vous gérez vous-même
  • Possibilité de proposer des services de revente
  • Contrôle total sur les mises à jour et les fonctionnalités

Cette approche est particulièrement intéressante si l’on considère que notre configuration offre des fonctionnalités pour l’entreprise qui coûteraient généralement beaucoup plus cher sous forme d’abonnements SaaS, comme les avantages de coût présentés dans notre analyse de l’auto-hébergement de n8n.

De la configuration à la décision opérationnelle

La question pratique n’est pas de savoir si une plateforme peut être configurée. Il s’agit de déterminer si l’équipe peut expliquer qui en est responsable, détecter les dérives, rétablir le service sans improviser et démontrer que le changement a amélioré le résultat visé. Nous associons donc chaque configuration à un responsable, une référence initiale, une procédure de retour à la version précédente et une période de vérification après action. Une correction ponctuelle devient ainsi une capacité opérationnelle. Le dossier de production doit indiquer la raison du changement, les éléments examinés, le périmètre exact et la personne autorisée à l’arrêter ou à l’annuler. Après la mise en service, comparez les indicateurs convenus à la référence initiale et conservez le résultat dans le dossier du changement. Si l’amélioration attendue n’est pas au rendez-vous, le retour à la version précédente constitue un résultat valable plutôt qu’un échec. Cette discipline compte davantage que toute valeur de configuration prise isolément. Le même dossier offre au prochain opérateur un point de départ fiable et fait de l’optimisation ultérieure une décision fondée sur des mesures plutôt qu’une nouvelle série de suppositions.

Analyses connexes

Pour aller plus loin