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.
- Reverse proxy in cascata: un’architettura per la produzione
- Ripristino di emergenza per servizi gestiti sulla propria infrastruttura: la nostra strategia di backup
- Oltre cento container Docker: i nostri controlli mensili sullo stato dei servizi
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
- 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
- Gestire più istanze di database sulla propria infrastruttura, su un unico server
- Un audit di sicurezza in più fasi: rafforzare un’applicazione Next.js
- La scelta del self-hosting: quando il SaaS costa più della propria infrastruttura
- Hetzner Cloud a fine 2026: piani e prezzi per il self-hosting di n8n, Windmill, Twenty e Ollama