Docker com múltiplos clientes: sem falhas compartilhadas por acidente
Um artigo de atualização com foco em produção que transforma orientações atuais dos fornecedores em controles operacionais, decisões de migração e critérios de lançamento verificáveis.
O que mudou em 2026
Voltamos a este tema porque os limites operacionais mudaram. Os princípios duradouros continuam válidos, mas as versões atuais tornam alguns atalhos anteriores incompletos ou arriscados. Este artigo parte da documentação oficial disponível em 14 de julho de 2026, distingue fatos de escolhas locais e trata cada alteração de configuração como uma intervenção controlada em produção.
Fontes primárias atuais
Como usamos esta atualização
Começamos pelas fontes primárias, registramos as versões efetivamente instaladas e definimos o resultado observável antes de alterar qualquer parâmetro. A menor intervenção reversível é testada em um ambiente representativo. Um comando concluído com êxito não é um critério de aceitação: o estado do serviço, a integridade dos dados, a latência, os limites de segurança e o tempo de reversão são. A sequência de diagnóstico ainda útil do artigo anterior é mantida como base operacional; os exemplos dependentes de versão devem ser confirmados na documentação atual.
- Empilhando proxies reversos: uma arquitetura para produção
- Recuperação de desastres para serviços auto-hospedados: nossa estratégia de backup
- Mais de cem contêineres Docker: nossa rotina mensal de verificação de integridade
Base operacional
Como criar um ambiente de desenvolvimento para múltiplos clientes baseado em modelos, com 16 serviços em contêineres, que funciona offline e permanece acessível online por meio de roteamento baseado em subdomínios. Os arquivos de configuração, scripts e exemplos de comandos a seguir vêm do guia original e foram corrigidos onde necessário.
Gerenciar ambientes de desenvolvimento para múltiplos clientes frequentemente significa escolher entre configurações manuais complexas ou soluções caras em nuvem. Implantações manuais são demoradas e propensas a erros. Plataformas em nuvem são convenientes, mas criam dependência de fornecedor e custos contínuos que aumentam conforme o uso.
Hoje, vamos percorrer a construção de um stack de desenvolvimento escalável para múltiplos clientes que oferece os dois benefícios: forte isolamento entre os ambientes dos clientes e recursos de implantação automatizada, mantendo o controle total sobre sua infraestrutura. Essa abordagem se baseia em nossa filosofia de soluções auto-hospedadas — como mostramos ao auto-hospedar o n8n para automação de fluxos de trabalho e implantar o Windmill com Docker para ter controle operacional completo.
As ferramentas que estamos usando
Vamos começar entendendo o que cada peça faz em nossa arquitetura abrangente de 16 contêineres:
Docker: sua base de conteinerização
O Docker fornece o isolamento e a consistência de que precisamos para ambientes para múltiplos clientes. Cada cliente recebe seus próprios contêineres com configurações idênticas, o que ajuda a garantir que o que funciona em desenvolvimento também funcione em produção. Pense nisso como ter múltiplos servidores separados em execução no mesmo hardware, embora ainda compartilhem CPU, memória, disco e kernel.
A vantagem principal? Forte isolamento entre clientes. Os dados, configurações e personalizações de cada cliente ficam em contêineres, redes e volumes separados, de modo que não interferem com os dos demais. Isso importa ao lidar com múltiplos clientes empresariais com diferentes requisitos e necessidades de segurança.
Traefik: proxy reverso e balanceador de carga inteligente
O Traefik age como um direcionador de tráfego inteligente, roteando automaticamente requisições para o ambiente de cliente correto com base em nomes de domínio. Em vez de configurar manualmente regras complexas de Apache ou Nginx, o Traefik lê rótulos dos seus contêineres Docker e configura o roteamento automaticamente.
Pense no Traefik como uma recepcionista que sabe a qual escritório (contêiner) cada visitante (requisição) deve ir, sem que você precise dar direções a cada vez. Em nossa configuração, o Traefik lida com terminação SSL, descoberta automática de serviços e fornece painéis de monitoramento detalhados.
Cloudflare Tunnels: acesso externo seguro
Cloudflare Tunnels fornecem acesso seguro ao seu stack de desenvolvimento local sem configurações complexas de firewall ou VPNs. Cada domínio de cliente recebe seu próprio túnel, mantendo separados os caminhos de acesso externo de cada cliente.
Os ambientes de desenvolvimento permanecem locais e seguros, mas os clientes podem acessar seus serviços específicos de qualquer lugar com a autenticação adequada, como fizemos ao configurar o acesso externo seguro no guia de hospedagem do n8n.
O stack completo de serviços: tudo de que seus clientes precisam
Nosso stack para múltiplos clientes inclui quatro categorias principais de serviços, distribuídas em 16 contêineres por cliente:
Automação de fluxos de trabalho e lógica de negócios:
- n8n: Plataforma completa de automação de fluxos de trabalho para automação de processos de negócio
- Authentik: autenticação única (SSO) e gerenciamento de identidades de nível empresarial (3 contêineres: servidor, worker, cache Redis)
Banco de dados e serviços de backend:
- PostgreSQL: Backend de banco de dados robusto para todos os serviços, com pool de conexões otimizado
- Stack Supabase: backend como serviço completo com 6 contêineres especializados (Studio, Meta, Auth, REST API, Realtime, Kong Gateway)
- NocoDB: interface de banco de dados sem código para gerenciamento dos dados do cliente
IA e inteligência:
- Ollama: modelos de linguagem de IA locais com aceleração por GPU para automação inteligente
- Qdrant (opcional): banco de dados vetorial para fluxos de trabalho avançados de IA e busca por similaridade
Infraestrutura e monitoramento:
- Cloudflare Tunnel: conectividade externa segura
- Traefik: proxy reverso com SSL automático e painel de monitoramento
Como tudo funciona em conjunto
Este é o fluxo completo quando um cliente acessa seu ambiente:
- O cliente acessa seu domínio personalizado (por exemplo,
workflows.client-a.com)
- Cloudflare Tunnel encaminha a requisição à sua instância local do Traefik
- O Traefik lê o domínio, aplica middleware (autenticação, SSL, limitação de requisições) e encaminha a requisição ao contêiner correto do cliente
- Se configurado, Authentik gerencia a autenticação SSO em todos os serviços
- O cliente acessa seu próprio ambiente isolado, com seus dados e configurações
- Os ambientes dos demais clientes não são acessíveis pelas rotas deste cliente
Tudo fica organizado e separado, com uma estrutura de subdomínios própria para cada cliente, como auth.client-a.com, database.client-a.com, backend.client-a.com etc.
Configuração: os passos práticos
Preparando a base
Primeiro, você precisará do Docker Desktop instalado e de uma estrutura de gerenciamento de domínios. Recomendamos configurar uma estrutura DNS com curinga para facilitar a integração de novos clientes:
# 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
Criando o sistema de modelos
O diferencial está na abordagem baseada em modelos. Em vez de configurar cada cliente manualmente, criamos modelos que podem ser implantados imediatamente com configurações específicas de cada cliente.
Crie a estrutura completa de diretórios:
mkdir -p development-stack/{template,deployments}
cd development-stack/template
# Create service-specific configuration directories
mkdir -p {traefik,authentik,supabase,init}
Configuração completa do modelo com múltiplos serviços
Crie um modelo completo de docker-compose.yml com todos os 16 serviços:
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:
Modelo completo de ambiente
Crie .env.template com todas as variáveis específicas dos clientes:
# Client Configuration
CLIENT_NAME=CLIENT_NAME_PLACEHOLDER
CLIENT_DOMAIN=CLIENT_DOMAIN_PLACEHOLDER
TENANT_PREFIX=CLIENT_PREFIX_PLACEHOLDER
TENANT_NETWORK=CLIENT_NETWORK_PLACEHOLDER
# Compose project name: keeps volume names and Traefik's container filter per client
COMPOSE_PROJECT_NAME=CLIENT_PREFIX_PLACEHOLDER
# Service Domains (Subdomain-based routing)
N8N_DOMAIN=workflows.CLIENT_DOMAIN_PLACEHOLDER
NOCODB_DOMAIN=database.CLIENT_DOMAIN_PLACEHOLDER
SUPABASE_DOMAIN=backend.CLIENT_DOMAIN_PLACEHOLDER
AUTHENTIK_DOMAIN=auth.CLIENT_DOMAIN_PLACEHOLDER
# Host Ports (localhost only, offset per client by the deployment script)
TRAEFIK_DASHBOARD_PORT=8080
POSTGRES_PORT=5432
# Admin Configuration
ADMIN_EMAIL=admin@CLIENT_DOMAIN_PLACEHOLDER
# Database Configuration
POSTGRES_DB=main_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=SECURE_PASSWORD_PLACEHOLDER
# Service-Specific Passwords
NOCODB_ADMIN_PASSWORD=NOCODB_PASSWORD_PLACEHOLDER
SUPABASE_JWT_SECRET=SUPABASE_JWT_PLACEHOLDER
# Authentik SSO Configuration
AUTHENTIK_SECRET_KEY=AUTHENTIK_SECRET_PLACEHOLDER
AUTHENTIK_TAG=2025.8.6
# n8n Configuration
N8N_PROTOCOL=https
N8N_SECURE_COOKIE=true
# Cloudflare Integration (tunnel token, and an API token with Zone:DNS:Edit for certificates)
CLOUDFLARE_TOKEN=CLOUDFLARE_TOKEN_PLACEHOLDER
CF_DNS_API_TOKEN=CF_DNS_API_TOKEN_PLACEHOLDER
# Authentication Middleware (set to 'auth-global' for SSO, leave empty for no auth)
AUTH_MIDDLEWARE=
Scripts de inicialização dos bancos de dados
Crie a inicialização completa dos bancos em 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'
Configuração do Supabase Kong Gateway
Crie supabase/kong.yml para o roteamento do gateway de API:
_format_version: "3.0"
services:
- name: auth-v1-open
url: http://supabase-auth:9999/verify
plugins:
- name: cors
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
methods:
- POST
- OPTIONS
- name: auth-v1-open-callback
url: http://supabase-auth:9999/callback
plugins:
- name: cors
routes:
- name: auth-v1-open-callback
strip_path: true
paths:
- /auth/v1/callback
methods:
- GET
- POST
- OPTIONS
- name: auth-v1
_comment: "GoTrue: /auth/v1/* -> http://supabase-auth:9999/*"
url: http://supabase-auth:9999/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
- name: rest-v1
_comment: "PostgREST: /rest/v1/* -> http://supabase-rest:3000/*"
url: http://supabase-rest:3000/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
- name: realtime-v1
_comment: "Realtime: /realtime/v1/* -> ws://supabase-realtime:4000/socket/*"
url: http://supabase-realtime:4000/socket/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
routes:
- name: realtime-v1-all
strip_path: true
paths:
- /realtime/v1/
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
consumers:
- username: anon
keyauth_credentials:
- key: your-anon-key-here
- username: service_role
keyauth_credentials:
- key: your-service-role-key-here
plugins:
- name: cors
config:
origins:
- "*"
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
headers:
- Accept
- Accept-Version
- Content-Length
- Content-MD5
- Content-Type
- Date
- X-Auth-Token
- Authorization
- X-Forwarded-For
- X-Forwarded-Proto
- X-Forwarded-Port
exposed_headers:
- X-Auth-Token
credentials: true
max_age: 3600
Script de implantação automatizada
O script completo de implantação que cria novos ambientes de clientes em minutos:
#!/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"
Usando seu stack para múltiplos clientes
Implantando ambientes para novos clientes
Criar um ambiente para um novo cliente fica simples com nosso script completo:
# 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"
Cada implantação cria:
- Rede Docker dedicada com 16 contêineres
- Volumes de dados separados para armazenamento persistente
- Contêineres de serviços exclusivos com monitoramento de integridade
- Configuração individual de Cloudflare Tunnel
- Roteamento de domínios personalizados com certificados SSL
- Infraestrutura SSO de nível empresarial pronta para ativação
Gerenciando múltiplos ambientes
Monitore todos os ambientes dos clientes de um ponto central:
# Check all running environments across clients
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(client|startup)"
# View comprehensive logs for specific client
cd deployments/client-a.com
docker compose logs -f --tail=50 n8n
# Health check all services for a client
docker compose ps
docker compose exec postgres pg_isready
# Restart specific services
docker compose restart nocodb supabase-studio
# Update all services to the image versions now pinned in docker-compose.yml
docker compose pull && docker compose up -d
Arquitetura dos contêineres em detalhe
Nossa arquitetura completa de 16 contêineres por cliente inclui:
Camada de infraestrutura (4 contêineres):
cloudflare-tunnel: conectividade externa segura
traefik: proxy reverso com SSL automático e descoberta de serviços
postgres: banco de dados central com pool de conexões
whoami: monitoramento de integridade e verificação do roteamento
Camada de aplicação (7 contêineres):
n8n: automação de fluxos de trabalho com backend PostgreSQL
nocodb: interface de banco de dados sem código
supabase-studio: painel de desenvolvimento do backend
supabase-meta: serviço de introspecção do banco de dados
supabase-auth: autenticação e gerenciamento de usuários
supabase-rest: API REST gerada automaticamente
supabase-realtime: assinaturas e atualizações em tempo real
Camada de IA e gateway (2 contêineres):
ollama: IA local com suporte a aceleração por GPU
supabase-kong: gateway de API com limitação de requisições e CORS
Camada de segurança empresarial (3 contêineres):
authentik-server: servidor de autenticação SSO
authentik-worker: tarefas em segundo plano e notificações
authentik-redis: gerenciamento de sessões e cache
Dimensionando os recursos por cliente
Ajuste os recursos conforme as necessidades e os padrões de uso de cada 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]
Benefícios reais para sua empresa
Isolamento completo dos clientes com recursos empresariais
Cada cliente recebe seu próprio ambiente completo, incluindo SSO de nível empresarial, recursos de IA e toda a infraestrutura de backend. Dados, configurações, personalizações e políticas de segurança ficam separados. Um problema no nível da aplicação no ambiente de um cliente fica restrito a esse ambiente, mas, como todos os clientes compartilham um host, problemas no nível do host, como o esgotamento de CPU, memória ou disco, ainda podem afetar todos. Isso é semelhante ao isolamento das nossas implantações individuais de n8n.
Integração rápida de clientes com todos os recursos
Novos clientes podem ter um stack completo de desenvolvimento e automação em funcionamento em menos de 10 minutos. O script de implantação automatiza a configuração, a geração de credenciais e a inicialização dos serviços; os registros DNS do Cloudflare e o SSO opcional ainda exigem breves etapas manuais.
Custos empresariais previsíveis
Após a configuração inicial, não há custos de hospedagem por cliente além da infraestrutura básica. Ao contrário de soluções SaaS que cobram por usuário, fluxo de trabalho ou chamada de API, você paga um valor fixo pelo servidor e pode executar tantos ambientes de clientes com todos os recursos empresariais quanto os recursos do servidor permitirem.
Consistência profissional da marca
Cada cliente recebe domínios próprios com sua marca e subdomínios profissionais (workflows.client.com, auth.client.com etc.) e pode personalizar totalmente os ambientes. Sem rodapés “powered by” ou interfaces compartilhadas que diluam sua identidade de marca.
Integração com n8n: automação de fluxos de trabalho empresariais em escala
É aqui que a combinação ganha força. Assim como mostramos na auto-hospedagem do n8n para automação de fluxos de trabalho, essa configuração para múltiplos clientes oferece a cada um sua própria instância completa do n8n, integrada a um stack empresarial completo.
Cada cliente pode criar fluxos de trabalho sofisticados que:
- Conectam seus próprios bancos de dados (NocoDB, Supabase PostgreSQL)
- Usam seus próprios modelos de IA (Ollama) para automação inteligente
- Autenticam via SSO empresarial (Authentik)
- Integram suas ferramentas de negócios e APIs específicas
- Processam seus dados dentro de seu próprio ambiente isolado
A combinação cria uma plataforma poderosa de entrega de serviços aos clientes, que permite:
- Implantar rapidamente recursos padronizados de automação
- Personalizar fluxos de trabalho por cliente sem afetar os demais
- Ampliar a prestação de serviços sem aumentos lineares de custos
- Manter a soberania completa dos dados de cada cliente
- Oferecer controle de acesso baseado em SSO como base para o trabalho de segurança e conformidade
Essa abordagem se baseia nos mesmos princípios do nosso guia de configuração do Windmill com Docker, mas os amplia para uma arquitetura completa para múltiplos clientes.
Opções avançadas de configuração
Implementando SSO empresarial com Authentik
Habilite a autenticação única em todos os serviços dos clientes configurando a autenticação encaminhada do 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"
Depois, atualize os rótulos dos serviços para usar o middleware:
labels:
- "traefik.http.routers.n8n.middlewares=auth-global"
- "traefik.http.routers.nocodb.middlewares=auth-global"
- "traefik.http.routers.supabase-studio.middlewares=auth-global"
Adicionando um banco de dados vetorial para IA avançada
Aprimore os recursos de IA com o banco de dados vetorial 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:
Implementando uma arquitetura híbrida de IA
Para obter o melhor desempenho, considere uma abordagem híbrida que combine IA em contêineres com execução 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
No macOS, isso importa porque o Docker Desktop não oferece suporte a GPUs em contêineres, enquanto o Ollama executado nativamente usa a aceleração por GPU diretamente. Em nossa configuração, isso melhorou o desempenho em cerca de 5 a 6 vezes, enquanto os demais serviços permaneceram em contêineres.
Stack de monitoramento e observabilidade
Adicione monitoramento completo por 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:
Problemas comuns e soluções
Erros “Service Unavailable” ou HTTP 502
Geralmente indicam que o Traefik não consegue acessar o contêiner de destino. Verifique se:
# 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
Problemas de resolução DNS
A configuração DNS com curinga é essencial para o roteamento por subdomínio:
# 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
Esgotamento de recursos entre múltiplos clientes
Monitore o uso de recursos em todos os ambientes dos clientes:
# 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}}"
Esgotamento do pool de conexões do banco de dados
Os limites de conexão do PostgreSQL podem ser atingidos com muitos clientes. Configure-os por implantação:
# 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
Problemas de configuração do SSO Authentik
Problemas comuns de configuração do SSO e suas soluções:
# 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
Problemas de conexão do Cloudflare Tunnel
Depure os problemas de conectividade do túnel:
# 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
Considerações sobre infraestrutura
Dimensionando a infraestrutura para múltiplos clientes
Para uma configuração típica que atende simultaneamente de 10 a 15 clientes com stacks completos de 16 contêineres:
Requisitos mínimos:
- CPU: de 16 a 24 núcleos (2 núcleos por ambiente de cliente ativo)
- RAM: de 64 a 128 GB (de 4 a 8 GB por cliente, conforme o uso de IA)
- Armazenamento: SSD NVMe com 2 TB+ (bancos de dados, modelos de IA e logs crescem com o tempo)
- Rede: conexão gigabit para acesso dos clientes com resposta rápida
Recomendado para produção:
- Servidor: Hetzner CCX62 ou similar (48 vCPU, 192 GB de RAM)
- Armazenamento: 4 TB NVMe com sistema automatizado de backup
- Rede: múltiplas conexões redundantes
- Monitoramento: stack completo de observabilidade com alertas
Estratégia de backup para ambientes com múltiplos clientes
Implemente backups automatizados por 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/
Reforço de segurança para produção
Implemente boas práticas abrangentes de segurança:
# 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"
Aplique middleware de segurança a todos os serviços dos clientes:
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"
Análise de custos: os números que importam
Custos tradicionais de SaaS (10 clientes empresariais com todos os recursos)
Custos mensais por cliente:
- n8n Pro: $50/mês por cliente = $500/mês
- Supabase Pro: $25/mês por cliente = $250/mês
- Plataforma sem código (Airtable): $20/mês por cliente = $200/mês
- SSO empresarial (Auth0): $23/mês por cliente = $230/mês
- Custos de API de IA (OpenAI): $50/mês por cliente = $500/mês
- Total: $1.680/mês = $20.160/ano
Custos do stack empresarial auto-hospedado para múltiplos clientes
Custos anuais de infraestrutura:
- Servidor em nuvem com vCPUs dedicadas (Hetzner CCX62): $350/mês = $4.200/ano
- Custos de domínios (10 clientes): $120/ano
- Cloudflare Pro (opcional): $240/ano
- Total: $4.560/ano
Economia anual: $15.600 (redução de custos de 77%)
Esses valores vêm do guia original de agosto de 2025. Em outubro de 2026, o plano B2C Essentials do Auth0 parte de $35/mês, e a Hetzner já não lista o CCX62; seu sucessor, o CCX63, com 48 vCPUs e 192 GB, está listado por €853,99/mês, sem IVA, incluindo um endereço IPv4 primário. Recalcule os dois cenários com os preços atuais antes de decidir.
Além disso, você obtém:
- Soberania e privacidade completas dos dados
- Personalização ilimitada e uso da marca do cliente
- Sem dependência de fornecedor nem limites de requisições dos planos SaaS
- Controles de segurança e conformidade que você mesmo gerencia
- Possibilidade de revender serviços
- Controle total sobre atualizações e funcionalidades
Isso é particularmente relevante porque nossa configuração fornece recursos empresariais que normalmente custariam muito mais em assinaturas SaaS, como mostramos na análise dos benefícios de custo da auto-hospedagem do n8n.
Da configuração à decisão operacional
A questão prática não é se uma plataforma pode ser configurada, mas se a equipe consegue explicar as responsabilidades, detectar desvios, recuperar o serviço sem improvisação e demonstrar que a mudança melhorou o resultado pretendido. Por isso, associamos cada mudança a um responsável, uma linha de base, um caminho de reversão e uma janela de verificação dos resultados. Isso transforma uma correção pontual em uma capacidade operacional. O registro de produção deve incluir o motivo da mudança, as evidências analisadas, o escopo exato e a pessoa autorizada a interrompê-la ou revertê-la. Após o lançamento, compare os indicadores acordados com a linha de base e guarde o resultado junto ao registro da mudança. Se a melhoria esperada não ocorrer, a reversão é um resultado válido, em vez de um fracasso. Essa disciplina importa mais do que qualquer valor individual de configuração. O mesmo registro oferece ao próximo operador um ponto de partida confiável e transforma a otimização posterior em uma decisão mensurada, em vez de outra rodada de suposições.
Artigos relacionados
- Auto-hospedagem de múltiplas instâncias de banco de dados em um único servidor
- Uma auditoria de segurança em múltiplas fases: fortalecendo uma aplicação Next.js
- A decisão de auto-hospedagem: quando o SaaS custa mais do que sua própria infraestrutura
- Hetzner Cloud no fim de 2026: planos e preços para hospedar n8n, Windmill, Twenty e Ollama por conta própria