tva
← Artigos

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.

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

Leitura complementar