tva
← Approfondimenti

Docker multi-tenant senza rischi condivisi involontari

Un approfondimento aggiornato e orientato alla produzione che traduce la documentazione dei fornitori in controlli operativi, decisioni di migrazione e criteri di rilascio verificabili.

Cosa è cambiato nel 2026

Torniamo su questo tema perché i confini operativi sono cambiati. I principi di fondo restano validi, ma le versioni attuali rendono alcune scorciatoie del passato incomplete o rischiose. Questo approfondimento parte dalla documentazione ufficiale disponibile al 14 luglio 2026, distingue i fatti dalle scelte locali e tratta ogni modifica di configurazione come un intervento controllato in produzione.

Fonti primarie aggiornate

Come utilizzare questo aggiornamento

Leggi prima le fonti, registra le versioni effettivamente distribuite e definisci il risultato osservabile prima di modificare un’impostazione. Prova il più piccolo intervento reversibile in un ambiente rappresentativo. Un comando eseguito con successo non è il criterio di accettazione: lo sono lo stato del servizio, l’integrità dei dati, la latenza, i confini di sicurezza e il tempo necessario per il rollback. Dove la guida precedente offre una sequenza diagnostica utile, la manteniamo di seguito come base operativa; gli esempi legati a una versione devono comunque essere verificati nella documentazione corrente.

Base operativa

Come creare un ambiente di sviluppo multi-tenant basato su modelli, con 16 servizi in container, che funziona offline ma resta accessibile online tramite instradamento basato sui sottodomini. I file di configurazione, gli script e gli esempi di comandi riportati di seguito provengono dalla guida originale, corretti dove necessario.

Gestire ambienti di sviluppo per più clienti spesso significa scegliere tra configurazioni manuali complesse e soluzioni cloud costose. Le distribuzioni manuali richiedono tempo e sono soggette a errori. Le piattaforme cloud sono comode, ma creano dipendenza dal fornitore e costi ricorrenti che crescono con l’utilizzo.

Oggi vediamo come costruire uno stack di sviluppo multi-tenant scalabile che offre forte isolamento tra gli ambienti dei clienti e distribuzione automatizzata, mantenendo il pieno controllo dell’infrastruttura. Questo approccio si basa sulla nostra filosofia di gestione dei servizi sulla propria infrastruttura, come abbiamo mostrato con il self-hosting di n8n per l’automazione dei flussi di lavoro e la distribuzione di Windmill con Docker per il pieno controllo operativo.

Gli strumenti che utilizziamo

Vediamo cosa fa ciascun componente nella nostra architettura completa a 16 container:

Docker: la base della containerizzazione

Docker offre isolamento e coerenza per gli ambienti multi-tenant. Ogni cliente ha i propri container con configurazioni identiche, il che aiuta a far sì che ciò che funziona in sviluppo funzioni anche in produzione. È come avere più server separati sullo stesso hardware, che continuano però a condividere CPU, memoria, disco e kernel.

Il vantaggio principale? Un forte isolamento tra i clienti. Dati, configurazioni e personalizzazioni di ogni cliente risiedono in container, reti e volumi separati, così non interferiscono con quelli degli altri. È importante quando si gestiscono più clienti aziendali con requisiti ed esigenze di sicurezza diversi.

Traefik: reverse proxy intelligente e bilanciatore di carico

Traefik dirige il traffico in modo intelligente, instradando automaticamente le richieste all’ambiente del cliente corretto in base al dominio. Anziché configurare manualmente complesse regole Apache o Nginx, legge le etichette dei container Docker e configura automaticamente l’instradamento.

Pensa a Traefik come a un addetto alla reception che sa a quale ufficio, cioè container, indirizzare ogni visitatore, cioè richiesta, senza dovergli dare indicazioni ogni volta. Nella nostra configurazione gestisce la terminazione SSL, il rilevamento automatico dei servizi e dashboard dettagliate di monitoraggio.

Cloudflare Tunnels: accesso esterno sicuro

Cloudflare Tunnels offre accesso sicuro allo stack di sviluppo locale senza configurazioni complesse del firewall o VPN. Ogni dominio cliente ha il proprio tunnel, mantenendo separati i percorsi di accesso esterno di ciascun cliente.

Il vantaggio è che gli ambienti di sviluppo restano locali e sicuri, ma i clienti possono accedere ai propri servizi da qualsiasi luogo con l’autenticazione appropriata, come nell’accesso esterno sicuro configurato nella nostra guida all’hosting di n8n.

Lo stack completo: tutto ciò che serve ai clienti

Il nostro stack multi-tenant comprende quattro categorie principali di servizi, distribuite su 16 container per cliente:

Automazione dei flussi di lavoro e logica aziendale:

  • n8n: piattaforma completa di automazione dei flussi di lavoro per automatizzare i processi aziendali
  • Authentik: single sign-on e gestione delle identità di livello aziendale, con 3 container: server, worker e cache Redis

Database e servizi backend:

  • PostgreSQL: backend database robusto che supporta tutti i servizi con un pool di connessioni ottimizzato
  • Supabase Stack: backend-as-a-service completo con 6 container specializzati: Studio, Meta, Auth, REST API, Realtime e Kong Gateway
  • NocoDB: interfaccia no-code del database per gestire i dati dei clienti

IA e funzionalità intelligenti:

  • Ollama: modelli linguistici IA locali con accelerazione GPU per l’automazione intelligente
  • Qdrant, opzionale: database vettoriale per flussi IA avanzati e ricerche per similarità

Infrastruttura e monitoraggio:

  • Cloudflare Tunnel: connettività esterna sicura
  • Traefik: reverse proxy con SSL automatico e dashboard di monitoraggio

Come funziona l’insieme

Ecco il flusso completo quando un cliente accede al proprio ambiente:

  • Il cliente apre il proprio dominio personalizzato, per esempio workflows.client-a.com
  • Cloudflare Tunnel instrada la richiesta all’istanza Traefik locale
  • Traefik legge il dominio, applica i middleware per autenticazione, SSL e limiti alla frequenza delle richieste, e inoltra la richiesta al container del cliente corretto
  • Authentik gestisce l’autenticazione SSO su tutti i servizi, se configurato
  • Il cliente accede al proprio ambiente isolato, con i propri dati e configurazioni
  • Gli ambienti degli altri clienti non sono raggiungibili tramite le route di questo cliente

Tutto resta organizzato e separato: ogni cliente ha la propria struttura di sottodomini, come auth.client-a.com, database.client-a.com e backend.client-a.com.

Configurazione pratica: i passaggi concreti

Preparare le basi

Per prima cosa servono Docker Desktop installato e una configurazione per gestire i domini. Consigliamo una struttura DNS wildcard per semplificare l’attivazione dei clienti:

# 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

Creare il sistema di modelli

Il punto chiave è l’approccio basato su modelli. Anziché configurare ogni cliente manualmente, creiamo modelli distribuibili immediatamente con configurazioni specifiche per cliente.

Crea la struttura completa delle directory:

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

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

Configurazione completa del modello multiservizio

Crea un modello completo di docker-compose.yml con tutti i 16 servizi:

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:

Modello completo dell’ambiente

Crea .env.template con tutte le variabili specifiche per cliente:

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

Script di inizializzazione del database

Crea l’inizializzazione completa del database in init/01-create-multiple-databases.sql:

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

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

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

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

\echo 'Multiple databases and extensions created successfully'

Configurazione del Kong Gateway di Supabase

Crea supabase/kong.yml per l’instradamento del gateway 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 di distribuzione automatizzata

Lo script completo di distribuzione crea nuovi ambienti cliente in pochi minuti:

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

Utilizzare lo stack multi-tenant

Distribuire gli ambienti per nuovi clienti

Con il nostro script completo, creare un nuovo ambiente cliente diventa semplice:

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

Ogni distribuzione crea:

  • Una rete Docker dedicata con 16 container
  • Volumi di dati separati per l’archiviazione persistente
  • Container di servizio dedicati con monitoraggio dello stato
  • Una configurazione Cloudflare Tunnel individuale
  • Instradamento del dominio personalizzato con certificati SSL
  • Un’infrastruttura SSO di livello aziendale pronta per l’attivazione

Gestire più ambienti

Monitora tutti gli ambienti cliente da un unico punto:

# 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

L’architettura dei container in dettaglio

L’architettura completa di 16 container per cliente comprende:

Livello infrastrutturale, 4 container:

  • cloudflare-tunnel: connettività esterna sicura
  • traefik: reverse proxy con SSL automatico e rilevamento dei servizi
  • postgres: database centrale con pool di connessioni
  • whoami: monitoraggio dello stato e verifica dell’instradamento

Livello applicativo, 7 container:

  • n8n: automazione dei flussi di lavoro con backend PostgreSQL
  • nocodb: interfaccia no-code del database
  • supabase-studio: dashboard per lo sviluppo del backend
  • supabase-meta: servizio di introspezione del database
  • supabase-auth: autenticazione e gestione degli utenti
  • supabase-rest: API REST generata automaticamente
  • supabase-realtime: sottoscrizioni e aggiornamenti in tempo reale

Livello IA e gateway, 2 container:

  • ollama: IA locale con supporto all’accelerazione GPU
  • supabase-kong: gateway API con limiti alla frequenza delle richieste e CORS

Livello di sicurezza aziendale, 3 container:

  • authentik-server: server di autenticazione SSO
  • authentik-worker: attività in background e notifiche
  • authentik-redis: gestione delle sessioni e cache

Scalare le risorse per cliente

Adatta le risorse alle esigenze e alle modalità d’uso del cliente:

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

Vantaggi concreti per l’azienda

Isolamento completo dei clienti con funzionalità aziendali

Ogni cliente ha un ambiente completo con SSO di livello aziendale, capacità IA e tutta l’infrastruttura backend. Dati, configurazioni, personalizzazioni e policy di sicurezza restano separati. Un problema a livello applicativo nell’ambiente di un cliente resta circoscritto a quell’ambiente, ma, poiché tutti i clienti condividono un host, problemi a livello di host come l’esaurimento di CPU, memoria o spazio su disco possono comunque coinvolgere tutti. È simile all’isolamento ottenuto con le nostre distribuzioni n8n individuali.

Attivazione rapida dei clienti con tutte le funzionalità

I nuovi clienti possono avere uno stack completo di sviluppo e automazione operativo in meno di 10 minuti. Lo script di distribuzione automatizza configurazione, generazione delle credenziali e inizializzazione dei servizi; i record DNS di Cloudflare e l’SSO facoltativo richiedono ancora brevi passaggi manuali.

Costi aziendali prevedibili

Dopo la configurazione iniziale, non ci sono costi di hosting per cliente oltre all’infrastruttura di base. A differenza delle soluzioni SaaS che fatturano per utente, flusso o chiamata API, paghi un importo fisso per il server e puoi eseguire tanti ambienti cliente con tutte le funzionalità aziendali quanti ne consentono le sue risorse.

Coerenza professionale del marchio

Ogni cliente ha domini del proprio marchio con sottodomini professionali, come workflows.client.com e auth.client.com, e può personalizzare completamente l’ambiente. Nessuna dicitura «powered by» a piè di pagina o interfaccia condivisa che ne diluisca l’identità di marca.

L’integrazione con n8n: automazione dei flussi aziendali su larga scala

Qui le possibilità diventano particolarmente ampie. Come abbiamo mostrato nel self-hosting di n8n per l’automazione dei flussi di lavoro, questa configurazione multi-tenant offre a ogni cliente un’istanza n8n completa integrata con un intero stack aziendale.

Ogni cliente può costruire flussi avanzati che:

  • Si collegano ai propri database, NocoDB e Supabase PostgreSQL
  • Usano i propri modelli IA, Ollama, per l’automazione intelligente
  • Si autenticano tramite SSO aziendale, Authentik
  • Si integrano con i propri strumenti aziendali e API
  • Elaborano i propri dati nel proprio ambiente isolato

La combinazione crea una potente piattaforma per erogare servizi ai clienti, con cui puoi:

  • Distribuire rapidamente capacità di automazione standardizzate
  • Personalizzare i flussi per cliente senza influire sugli altri
  • Scalare l’erogazione dei servizi senza aumenti lineari dei costi
  • Mantenere la piena sovranità dei dati per ogni cliente
  • Offrire un controllo degli accessi basato su SSO come base per il lavoro su sicurezza e conformità

Questo approccio riprende i principi della nostra guida a Windmill con Docker, estendendoli a un’architettura multi-tenant completa.

Opzioni di configurazione avanzate

Implementare SSO aziendale con Authentik

Abilita il single sign-on per tutti i servizi del cliente configurando la forward authentication di 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"

Poi aggiorna le etichette dei servizi per usare il middleware:

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

Aggiungere un database vettoriale per l’IA avanzata

Estendi le capacità IA con il database vettoriale 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:

Implementare un’architettura IA ibrida

Per prestazioni ottimali, valuta un approccio ibrido che combini IA in container e IA nativa:

# 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

Su macOS questo è importante perché Docker Desktop non supporta le GPU nei container, mentre Ollama eseguito nativamente usa direttamente l’accelerazione GPU. Nella nostra configurazione, le prestazioni sono migliorate di circa 5-6 volte, mentre gli altri servizi sono rimasti nei container.

Stack di monitoraggio e osservabilità

Aggiungi un monitoraggio completo per ogni cliente:

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:

Problemi comuni e soluzioni

Errori «Service Unavailable» o HTTP 502

Di solito Traefik non riesce a raggiungere il container di destinazione. Verifica che:

# 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

Problemi di risoluzione DNS

La configurazione DNS wildcard è essenziale per l’instradamento dei sottodomini:

# 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

Esaurimento delle risorse tra più clienti

Monitora l’uso delle risorse in tutti gli ambienti cliente:

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

Esaurimento del pool di connessioni al database

Con molti clienti si possono raggiungere i limiti di connessione PostgreSQL. Configurali per ogni distribuzione:

# 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

Problemi di configurazione SSO con Authentik

Problemi comuni nella configurazione SSO e relative soluzioni:

# 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

Problemi di connessione di Cloudflare Tunnel

Diagnostica i problemi di connettività del 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

Considerazioni infrastrutturali

Dimensionare l’infrastruttura per più clienti

Per una configurazione tipica che gestisce contemporaneamente 10-15 clienti con stack completi di 16 container:

Requisiti minimi:

  • CPU: 16-24 core, 2 core per ambiente cliente attivo
  • RAM: 64-128 GB, 4-8 GB per cliente secondo l’uso dell’IA
  • Archiviazione: SSD NVMe da almeno 2 TB, perché database, modelli IA e log crescono nel tempo
  • Rete: connessione Gigabit per un accesso reattivo dei clienti

Configurazione consigliata per la produzione:

  • Server: Hetzner CCX62 o simile, 48 vCPU e 192 GB di RAM
  • Archiviazione: 4 TB NVMe con sistema di backup automatizzato
  • Rete: più connessioni ridondanti
  • Monitoraggio: stack di osservabilità completo con avvisi

Strategia di backup per ambienti con più clienti

Implementa backup automatizzati per ogni cliente:

#!/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/

Rafforzare la sicurezza in produzione

Applica un insieme completo di buone pratiche di sicurezza:

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

Applica i middleware di sicurezza a tutti i servizi dei clienti:

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"

Analisi dei costi: i numeri che contano

Costi SaaS tradizionali, 10 clienti aziendali con tutte le funzionalità

Costi mensili per cliente:

  • n8n Pro: $50/mese per cliente = $500/mese
  • Supabase Pro: $25/mese per cliente = $250/mese
  • Piattaforma no-code, Airtable: $20/mese per cliente = $200/mese
  • SSO aziendale, Auth0: $23/mese per cliente = $230/mese
  • Costi delle API IA, OpenAI: $50/mese per cliente = $500/mese
  • Totale: $1.680/mese = $20.160/anno

Costi dello stack aziendale multi-tenant gestito sulla propria infrastruttura

Costi annuali dell’infrastruttura:

  • Server cloud con vCPU dedicate, Hetzner CCX62: $350/mese = $4.200/anno
  • Costi dei domini, 10 clienti: $120/anno
  • Cloudflare Pro, opzionale: $240/anno
  • Totale: $4.560/anno

Risparmio annuale: $15.600, riduzione dei costi del 77%

Queste cifre provengono dalla guida originale di agosto 2025. A ottobre 2026, il piano B2C Essentials di Auth0 parte da $35/mese e Hetzner non elenca più il CCX62; il suo successore CCX63, con 48 vCPU e 192 GB, è a listino a €853,99/mese IVA esclusa, incluso un indirizzo IPv4 primario. Ricalcola entrambe le opzioni con i prezzi attuali prima di decidere.

Inoltre ottieni:

  • Piena sovranità dei dati e privacy
  • Personalizzazione illimitata e possibilità di usare il proprio marchio
  • Nessuna dipendenza dal fornitore o limite di frequenza delle richieste imposto dai piani SaaS
  • Controlli di sicurezza e conformità che gestisci direttamente
  • Possibilità di offrire servizi di rivendita
  • Pieno controllo su aggiornamenti e funzionalità

Il vantaggio è particolarmente significativo considerando che la nostra configurazione offre funzionalità aziendali che in genere costerebbero molto di più in abbonamenti SaaS, come nei risparmi mostrati nella nostra analisi del self-hosting di n8n.

Dalla configurazione alla decisione operativa

La domanda pratica non è se una piattaforma possa essere configurata, ma se il team sappia spiegare le responsabilità, rilevare gli scostamenti, ripristinare il servizio senza improvvisare e dimostrare che la modifica ha migliorato il risultato previsto. Per questo associamo ogni modifica a un responsabile, un riferimento iniziale, un percorso di rollback e una finestra di verifica dei risultati. Una correzione isolata diventa così una capacità operativa. La documentazione di produzione deve includere il motivo della modifica, le evidenze esaminate, l’ambito esatto e la persona autorizzata a interromperla o annullarla. Dopo il rilascio, confrontiamo gli indicatori concordati con il riferimento iniziale e conserviamo il risultato insieme alla registrazione della modifica. Se il miglioramento atteso non si verifica, il rollback è un esito valido, non un fallimento. Questo rigore conta più di qualsiasi singolo valore di configurazione. La stessa documentazione offre all’operatore successivo un punto di partenza affidabile e rende le ottimizzazioni future decisioni misurate anziché un nuovo ciclo di supposizioni.

Approfondimenti correlati

Per approfondire