tva
← Artículos

Docker multiinquilino sin riesgos compartidos accidentales

Un artículo de seguimiento actualizado y orientado a la producción que convierte la documentación de los proveedores en controles operativos, decisiones de migración y criterios de despliegue verificables.

Qué ha cambiado en 2026

Retomamos este tema porque los límites operativos han cambiado. Los principios duraderos siguen siendo útiles, pero las versiones actuales hacen que algunos atajos anteriores sean incompletos o arriesgados. Este artículo de seguimiento parte de la documentación oficial disponible el 14 de julio de 2026, separa los hechos de las decisiones locales y trata cada cambio de configuración como una intervención controlada en producción.

Fuentes primarias actuales

Cómo utilizar esta actualización

Consulte primero las fuentes, registre las versiones realmente desplegadas y defina el resultado observable antes de modificar un ajuste. Pruebe la intervención reversible más pequeña en un entorno representativo. Un comando ejecutado con éxito no es un criterio de aceptación: sí lo son el estado del servicio, la integridad de los datos, la latencia, los límites de seguridad y el tiempo de reversión. La secuencia de diagnóstico del artículo anterior que aún resulta útil se conserva como base operativa; los ejemplos dependientes de una versión deben contrastarse con la documentación vigente.

Base operativa

Cómo crear un entorno de desarrollo multiinquilino basado en plantillas con 16 servicios en contenedores que funciona sin conexión y permanece accesible en línea a través de enrutamiento basado en subdominios. Los archivos de configuración, scripts y ejemplos de comandos que siguen proceden de la guía original y se han corregido donde hacía falta.

Gestionar entornos de desarrollo para múltiples clientes a menudo significa elegir entre configuraciones manuales complejas o soluciones en la nube costosas. Los despliegues manuales consumen tiempo y son propensos a errores. Las plataformas en la nube son convenientes pero crean dependencia de proveedores y costos continuos que escalan con el uso.

Hoy mostraremos cómo construir un stack de desarrollo multiinquilino escalable que reúne ambas ventajas: aislamiento sólido entre entornos de clientes y despliegue automatizado, manteniendo el control total de su infraestructura. Este enfoque se basa en nuestra filosofía de soluciones autoalojadas, al igual que autoalojar n8n para automatizar flujos de trabajo o desplegar Windmill con Docker permite un control operativo completo.

Las herramientas que utilizamos

Comencemos por entender qué hace cada pieza en nuestra arquitectura integral de 16 contenedores:

Docker: la base para trabajar con contenedores

Docker proporciona el aislamiento y la consistencia que necesitamos para entornos multiinquilinos. Cada cliente obtiene sus propios contenedores con configuraciones idénticas, lo que ayuda a que lo que funciona en desarrollo también funcione en producción. Piense en ello como varios servidores separados que se ejecutan en el mismo hardware, aunque siguen compartiendo CPU, memoria, disco y kernel.

¿La ventaja clave? Un aislamiento sólido entre clientes. Los datos, configuraciones y personalizaciones de cada cliente residen en contenedores, redes y volúmenes separados, por lo que no interfieren con los de otro. Esto importa al trabajar con varios clientes empresariales con diferentes requisitos y necesidades de seguridad.

Traefik: proxy inverso y balanceador de carga inteligente

Traefik actúa como un director de tráfico inteligente, enrutando automáticamente las solicitudes al entorno de cliente correcto según los nombres de dominio. En lugar de configurar manualmente reglas complejas de Apache o Nginx, Traefik lee las etiquetas de sus contenedores Docker y configura el enrutamiento automáticamente.

Piense en Traefik como un recepcionista que sabe exactamente a qué oficina (contenedor) debe ir cada visitante (solicitud), sin que usted tenga que dar indicaciones cada vez. En nuestra configuración, Traefik maneja la terminación SSL, el descubrimiento automático de servicios y proporciona paneles de monitoreo detallados.

Cloudflare Tunnels: acceso externo seguro

Cloudflare Tunnels ofrece acceso seguro a su stack de desarrollo local sin configuraciones complejas de firewall o VPN. Cada dominio de cliente obtiene su propio túnel, lo que mantiene separadas las rutas de acceso externo de cada cliente.

La ventaja es que los entornos de desarrollo permanecen locales y seguros, pero los clientes pueden acceder a sus servicios específicos desde cualquier lugar con la autenticación adecuada, como en el acceso externo seguro de nuestra guía de alojamiento de n8n.

El stack completo de servicios: todo lo que sus clientes necesitan

Nuestro stack multiinquilino incluye cuatro categorías principales de servicios en 16 contenedores por cliente:

Automatización de flujos de trabajo y lógica de negocio:

  • n8n: plataforma completa de automatización de flujos de trabajo para la automatización de procesos empresariales
  • Authentik: inicio de sesión único y gestión de identidad de nivel empresarial (3 contenedores: servidor, worker, caché Redis)

Servicios de base de datos y backend:

  • PostgreSQL: backend de base de datos sólido que da soporte a todos los servicios con gestión optimizada de conexiones reutilizables
  • Supabase Stack: backend como servicio completo con 6 contenedores especializados (Studio, Meta, Auth, REST API, Realtime, Kong Gateway)
  • NocoDB: interfaz de base de datos sin código para la gestión de datos de clientes

IA e inteligencia:

  • Ollama: modelos de lenguaje de IA local con aceleración GPU para automatización inteligente
  • Qdrant (opcional): base de datos vectorial para flujos de trabajo de IA avanzados y búsqueda por similitud

Infraestructura y monitoreo:

  • Cloudflare Tunnel: conectividad externa segura
  • Traefik: proxy inverso con SSL automático y panel de monitoreo

Cómo funciona todo en conjunto

Aquí está el flujo completo cuando un cliente accede a su entorno:

  • El cliente navega a su dominio personalizado (por ejemplo, workflows.client-a.com)
  • Cloudflare Tunnel enruta la solicitud a su instancia local de Traefik
  • Traefik lee el dominio, aplica middleware (autenticación, SSL, límites de solicitudes) y reenvía al contenedor de cliente correcto
  • Authentik gestiona la autenticación SSO en todos los servicios si está configurado
  • El cliente obtiene su propio entorno aislado con sus datos y configuraciones
  • Los entornos de otros clientes no son accesibles a través de las rutas de este cliente

Todo se mantiene organizado y separado, y cada cliente dispone de su propia estructura de subdominios como auth.client-a.com, database.client-a.com, backend.client-a.com, etc.

Configuración: pasos prácticos

Preparar la base

Primero, necesitará Docker Desktop instalado y una configuración de gestión de dominios. Recomendamos configurar una estructura DNS con comodín para una fácil incorporación de 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

Creación del sistema de plantillas

La clave es un enfoque basado en plantillas. En lugar de configurar manualmente cada cliente, creamos plantillas que pueden desplegarse instantáneamente con configuraciones específicas del cliente.

Cree la estructura de directorios completa:

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

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

Configuración completa de la plantilla para varios servicios

Cree una plantilla completa de docker-compose.yml con los 16 servicios:

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:

Plantilla de entorno completa

Cree .env.template con todas las variables específicas de cada cliente:

# Client Configuration
CLIENT_NAME=CLIENT_NAME_PLACEHOLDER
CLIENT_DOMAIN=CLIENT_DOMAIN_PLACEHOLDER
TENANT_PREFIX=CLIENT_PREFIX_PLACEHOLDER
TENANT_NETWORK=CLIENT_NETWORK_PLACEHOLDER
# Compose project name: keeps volume names and Traefik's container filter per client
COMPOSE_PROJECT_NAME=CLIENT_PREFIX_PLACEHOLDER

# Service Domains (Subdomain-based routing)
N8N_DOMAIN=workflows.CLIENT_DOMAIN_PLACEHOLDER
NOCODB_DOMAIN=database.CLIENT_DOMAIN_PLACEHOLDER
SUPABASE_DOMAIN=backend.CLIENT_DOMAIN_PLACEHOLDER
AUTHENTIK_DOMAIN=auth.CLIENT_DOMAIN_PLACEHOLDER

# Host Ports (localhost only, offset per client by the deployment script)
TRAEFIK_DASHBOARD_PORT=8080
POSTGRES_PORT=5432

# Admin Configuration
ADMIN_EMAIL=admin@CLIENT_DOMAIN_PLACEHOLDER

# Database Configuration
POSTGRES_DB=main_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=SECURE_PASSWORD_PLACEHOLDER

# Service-Specific Passwords
NOCODB_ADMIN_PASSWORD=NOCODB_PASSWORD_PLACEHOLDER
SUPABASE_JWT_SECRET=SUPABASE_JWT_PLACEHOLDER

# Authentik SSO Configuration
AUTHENTIK_SECRET_KEY=AUTHENTIK_SECRET_PLACEHOLDER
AUTHENTIK_TAG=2025.8.6

# n8n Configuration
N8N_PROTOCOL=https
N8N_SECURE_COOKIE=true

# Cloudflare Integration (tunnel token, and an API token with Zone:DNS:Edit for certificates)
CLOUDFLARE_TOKEN=CLOUDFLARE_TOKEN_PLACEHOLDER
CF_DNS_API_TOKEN=CF_DNS_API_TOKEN_PLACEHOLDER

# Authentication Middleware (set to 'auth-global' for SSO, leave empty for no auth)
AUTH_MIDDLEWARE=

Scripts de inicialización de base de datos

Prepare la inicialización completa de la base de datos en 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'

Configuración de la puerta de enlace Kong de Supabase

Cree supabase/kong.yml para configurar el enrutamiento de la puerta de enlace 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 despliegue automatizado

El script de despliegue completo que crea nuevos entornos de cliente en 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"

Uso de su stack multiinquilino

Despliegue de entornos para nuevos clientes

Crear un nuevo entorno de cliente resulta sencillo con nuestro 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 despliegue crea:

  • Red Docker dedicada con 16 contenedores
  • Volúmenes de datos separados para almacenamiento persistente
  • Contenedores de servicios propios con monitoreo de estado
  • Configuración individual de túnel de Cloudflare
  • Enrutamiento de dominio personalizado con certificados SSL
  • Infraestructura SSO de nivel empresarial lista para activación

Gestión de varios entornos

Monitoree todos los entornos de clientes desde una ubicación 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

Análisis detallado de la arquitectura de contenedores

Nuestra arquitectura completa de 16 contenedores por cliente incluye:

Capa de infraestructura (4 contenedores):

  • cloudflare-tunnel: conectividad externa segura
  • traefik: proxy inverso con SSL automático y descubrimiento de servicios
  • postgres: base de datos central con conexiones reutilizables
  • whoami: Monitoreo de estado y verificación de enrutamiento

Capa de aplicación (7 contenedores):

  • n8n: automatización de flujos de trabajo con backend PostgreSQL
  • nocodb: interfaz de base de datos sin código
  • supabase-studio: panel de desarrollo backend
  • supabase-meta: servicio de introspección de base de datos
  • supabase-auth: autenticación y gestión de usuarios
  • supabase-rest: API REST autogenerada
  • supabase-realtime: suscripciones y actualizaciones en tiempo real

Capa de IA y puerta de enlace (2 contenedores):

  • ollama: IA local con soporte de aceleración GPU
  • supabase-kong: puerta de enlace de API con límites de solicitudes y CORS

Capa de seguridad empresarial (3 contenedores):

  • authentik-server: servidor de autenticación SSO
  • authentik-worker: tareas en segundo plano y notificaciones
  • authentik-redis: gestión de sesiones y caché

Escalado de recursos por cliente

Ajuste los recursos según las necesidades y patrones de uso del cliente:

# High-performance client configuration (docker-compose.override.yml)
services:
  n8n:
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 8G
        reservations:
          cpus: '2.0'
          memory: 4G

  postgres:
    command: postgres -c max_connections=200 -c shared_buffers=1GB -c effective_cache_size=3GB
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G

  ollama:
    deploy:
      resources:
        limits:
          memory: 32G
        reservations:
          memory: 16G
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

Beneficios reales para su negocio

Aislamiento completo de clientes con funciones empresariales

Cada cliente dispone de un entorno completo con SSO de nivel empresarial, capacidades de IA e infraestructura de backend. Los datos, las configuraciones, las personalizaciones y las políticas de seguridad se mantienen separados. Un problema a nivel de aplicación en el entorno de un cliente queda dentro de ese entorno, pero, como todos los clientes comparten un mismo host, los problemas del host, como el agotamiento de CPU, memoria o disco, pueden afectar a todos. Es similar al aislamiento de nuestros despliegues individuales de n8n.

Incorporación rápida de clientes con conjunto completo de funciones

Los nuevos clientes pueden disponer de un stack completo de desarrollo y automatización operativo en menos de 10 minutos. El script de despliegue automatiza la configuración, la generación de credenciales y la inicialización de servicios; los registros DNS de Cloudflare y el SSO opcional siguen requiriendo breves pasos manuales.

Costos empresariales predecibles

Después de la configuración inicial, no hay costos de alojamiento por cliente más allá de su infraestructura base. A diferencia de las soluciones SaaS que cobran por usuario, por flujo de trabajo o por llamada a API, paga un importe fijo por el servidor y puede ejecutar tantos entornos de cliente con funciones empresariales completas como permitan sus recursos.

Consistencia de marca profesional

Cada cliente obtiene sus propios dominios de marca con subdominios profesionales (workflows.client.com, auth.client.com, etc.) y puede personalizar completamente sus entornos. Sin pies de página «desarrollado con» ni interfaces compartidas que diluyan la identidad de su marca.

La integración con n8n: automatización de flujos de trabajo empresariales a escala

Aquí es donde las cosas se vuelven realmente poderosas. Tal como hemos demostrado cómo autoalojar n8n para automatización de flujos de trabajo, esta configuración multiinquilino proporciona a cada cliente su propia instancia completa de n8n integrada con un stack empresarial completo.

Cada cliente puede construir flujos de trabajo sofisticados que:

  • Se conecten a sus propias bases de datos (NocoDB, PostgreSQL de Supabase)
  • Usen sus propios modelos de IA (Ollama) para automatización inteligente
  • Se autentiquen a través de SSO empresarial (Authentik)
  • Se integren con las herramientas y API específicas de su negocio
  • Procesen sus datos dentro de su propio entorno aislado

Esta combinación crea una potente plataforma de prestación de servicios a clientes que permite:

  • Desplegar capacidades de automatización estandarizadas rápidamente
  • Personalizar flujos de trabajo por cliente sin afectar a otros
  • Ampliar la prestación de sus servicios sin aumentos de costos lineales
  • Mantener la soberanía completa de datos para cada cliente
  • Ofrecer control de acceso basado en SSO como base para el trabajo de seguridad y cumplimiento normativo

Este enfoque se basa en los mismos principios que usamos en nuestra guía de configuración Docker de Windmill, pero lo extiende a una arquitectura multiinquilino completa.

Opciones de configuración avanzada

Implementación de SSO empresarial con Authentik

Habilite el inicio de sesión único en todos los servicios del cliente configurando la autenticación delegada de 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"

Después, actualice las etiquetas de sus servicios para usar el middleware:

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

Incorporación de una base de datos vectorial para IA avanzada

Mejore las capacidades de IA con la base de datos vectorial 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:

Implementación de una arquitectura de IA híbrida

Para un rendimiento óptimo, considere un enfoque híbrido que combine IA en contenedores y 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

En macOS esto importa porque Docker Desktop no admite GPU en los contenedores, mientras que Ollama ejecutado de forma nativa utiliza directamente la aceleración por GPU. En nuestra configuración, esto mejoró el rendimiento aproximadamente entre 5 y 6 veces, mientras los demás servicios siguieron en contenedores.

Stack de monitoreo y observabilidad

Añada monitoreo 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 comunes y soluciones

Errores "Service Unavailable" o HTTP 502

Esto suele indicar que Traefik no puede acceder al contenedor de destino. Compruebe lo siguiente:

# 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 resolución DNS

La configuración DNS con comodín es crucial para el enrutamiento por subdominios:

# 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

Agotamiento de recursos entre múltiples clientes

Monitoree el uso de recursos en todos los entornos de 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}}"

Agotamiento del conjunto de conexiones de base de datos

Con muchos clientes pueden alcanzarse los límites de conexiones de PostgreSQL. Configure estos valores para cada despliegue:

# 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 configuración de SSO con Authentik

Problemas comunes de configuración de SSO y soluciones:

# 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 conexión del túnel de Cloudflare

Depure problemas de conectividad del 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

Consideraciones de infraestructura

Dimensionamiento de su infraestructura para varios clientes

Para una configuración típica que maneja 10-15 clientes simultáneamente con stacks completos de 16 contenedores:

Requisitos mínimos:

  • CPU: 16-24 núcleos (2 núcleos por entorno de cliente activo)
  • RAM: 64-128 GB (4-8 GB por cliente según el uso de IA)
  • Almacenamiento: SSD NVMe con 2 TB o más (las bases de datos, modelos de IA y registros crecen con el tiempo)
  • Red: conexión gigabit para que el cliente acceda sin demoras

Recomendado para producción:

  • Servidor: Hetzner CCX62 o similar (48 vCPU, 192 GB de RAM)
  • Almacenamiento: 4 TB NVMe con sistema automatizado de copias de seguridad
  • Red: múltiples conexiones redundantes
  • Monitoreo: stack completo de observabilidad con alertas

Estrategia de copias de seguridad para entornos de varios clientes

Implemente copias de seguridad automatizadas 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/

Refuerzo de la seguridad para producción

Implemente un conjunto completo de buenas prácticas de seguridad:

# 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 el middleware de seguridad a todos los servicios del cliente:

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álisis de costos: los números que importan

Costos SaaS tradicionales (10 clientes empresariales con conjuntos completos de funciones)

Costos mensuales por cliente:

  • n8n Pro: $50/mes por cliente = $500/mes
  • Supabase Pro: $25/mes por cliente = $250/mes
  • Plataforma sin código (Airtable): $20/mes por cliente = $200/mes
  • SSO empresarial (Auth0): $23/mes por cliente = $230/mes
  • Costos de API de IA (OpenAI): $50/mes por cliente = $500/mes
  • Total: $1 680/mes = $20 160/año

Costos del stack empresarial multiinquilino autoalojado

Costos anuales de infraestructura:

  • Servidor en la nube con vCPU dedicadas (Hetzner CCX62): $350/mes = $4 200/año
  • Costos de dominio (10 clientes): $120/año
  • Cloudflare Pro (opcional): $240/año
  • Total: $4 560/año

Ahorro anual: $15 600 (77 % de reducción de costos)

Estas cifras proceden de la guía original de agosto de 2025. A octubre de 2026, el plan B2C Essentials de Auth0 parte de $35/mes y Hetzner ya no incluye el CCX62 en su catálogo; su sucesor, el CCX63, con 48 vCPU y 192 GB, figura a €853,99/mes sin IVA, incluida una dirección IPv4 primaria. Recalcule ambas opciones con los precios actuales antes de decidir.

Además, obtiene:

  • Soberanía y privacidad completas de los datos
  • Personalización ilimitada y marca blanca
  • Sin dependencia de proveedores ni límites de solicitudes de los planes SaaS
  • Controles de seguridad y cumplimiento normativo que usted mismo administra
  • Capacidad de ofrecer servicios de reventa
  • Control total sobre actualizaciones y funciones

La ventaja es especialmente importante si considera que nuestra configuración ofrece funciones empresariales que normalmente costarían mucho más en suscripciones SaaS, como muestran los ahorros de nuestro análisis de autoalojamiento de n8n.

De la configuración a una decisión operativa

La cuestión práctica no es si una plataforma se puede configurar, sino si el equipo puede explicar quién es responsable, detectar desviaciones, recuperarse sin improvisar y demostrar que el cambio mejoró el resultado previsto. Por eso asociamos la configuración con un responsable, una línea de base, una ruta de reversión y una ventana de verificación del resultado. Así, una corrección puntual se convierte en una capacidad operativa. El registro de producción debe incluir el motivo del cambio, la evidencia revisada, el alcance exacto y la persona autorizada para detenerlo o revertirlo. Después del despliegue, compare los indicadores acordados con la línea de base y conserve el resultado junto al registro del cambio. Si no aparece la mejora esperada, revertir es un resultado válido, no un fracaso. Esta disciplina importa más que cualquier valor concreto de configuración. El mismo registro ofrece al siguiente operador un punto de partida fiable y convierte la optimización posterior en una decisión medida, no en otra ronda de conjeturas.

Artículos relacionados

Artículos relacionados