tva
← Insights

Dockerでマルチテナント開発スタックを構築する:スケーラブルなクライアントデプロイのための完全セットアップガイド

サブドメインベースのルーティングを通じてオフラインでもオンラインでもアクセス可能な、16のコンテナ化されたサービスを持つテンプレートベースのマルチテナント開発環境の構築方法

複数クライアントの開発環境管理は、複雑な手動セットアップか高額なクラウドソリューションのどちらかを選択しなければならないことがよくあります。手動デプロイは時間がかかり、エラーも発生しやすいです。クラウドプラットフォームは便利ですが、ベンダーロックインと使用量に応じて増加する継続的なコストが発生します。

今回は、クライアント環境間の完全な分離と自動デプロイ機能を両立しながら、インフラストラクチャの完全なコントロールを維持するスケーラブルなマルチテナント開発スタックの構築方法をご紹介します。このアプローチは、セルフホスティングソリューションの理念に基づいています。n8nをセルフホストしてワークフロー自動化を実現する方法や、DockerでWindmillをデプロイする方法で完全な運用コントロールを実現するのと同様のコンセプトです。

使用するツール

まず、包括的な16コンテナアーキテクチャにおける各コンポーネントの役割を理解しましょう。

Docker:コンテナ化の基盤

Dockerは、マルチテナント環境に必要な分離性と一貫性を提供します。各クライアントは同一の設定を持つ独自のコンテナを取得するため、開発環境で動作するものが本番環境でも確実に動作します。同じハードウェア上で複数の完全に独立したサーバーを稼働させているようなものと考えてください。

主なメリットは、クライアント間の完璧な分離です。あるクライアントのデータ、設定、カスタマイズが他のクライアントに干渉することはありません。異なる要件やセキュリティニーズを持つ複数のビジネスクライアントを扱う際に、これは非常に重要です。

Traefik:スマートリバースプロキシとロードバランサー

Traefikは、ドメイン名に基づいてリクエストを適切なクライアント環境に自動的にルーティングするインテリジェントなトラフィック管理ツールです。複雑なApacheやNginxのルールを手動で設定する代わりに、TraefikがDockerコンテナのラベルを読み取り、ルーティングを自動的に設定します。

Traefikは、各訪問者(リクエスト)をどのオフィス(コンテナ)に案内すべきかを正確に知っている、いわばスマートな受付のようなものです。毎回指示を出す必要はありません。このセットアップでは、TraefikがSSL終端、自動サービスディスカバリを処理し、詳細なモニタリングダッシュボードを提供します。

Cloudflareトンネル:安全な外部アクセス

Cloudflareトンネルは、複雑なファイアウォール設定やVPNなしで、ローカル開発スタックへの安全なアクセスを提供します。各クライアントドメインは独自のトンネルを持ち、エンタープライズグレードのセキュリティを維持しながらネットワークレベルでの完全な分離を実現します。

優れた点は、開発環境がローカルかつ安全な状態を維持しながら、適切な認証によりクライアントがどこからでも特定のサービスにアクセスできることです。n8nホスティングガイドで安全な外部アクセスを設定したのと同様のアプローチです。

完全なサービススタック:クライアントが必要とするすべて

マルチテナントスタックには、クライアントごとに16コンテナにわたる7つのコアサービスカテゴリが含まれます。

ワークフロー自動化とビジネスロジック:

  • n8n:ビジネスプロセス自動化のための完全なワークフロー自動化プラットフォーム
  • Authentik:エンタープライズグレードのシングルサインオンとID管理(サーバー、ワーカー、Redisキャッシュの3コンテナ)

データベースとバックエンドサービス:

  • PostgreSQL:最適化されたコネクションプーリングですべてのサービスをサポートする堅牢なデータベースバックエンド
  • Supabaseスタック:5つの専用コンテナ(Studio、Auth、REST API、Realtime、Kong Gateway)による完全なBaaS(Backend-as-a-Service)
  • NocoDB:クライアントデータ管理のためのノーコードデータベースインターフェース

AIとインテリジェンス:

  • Ollama:インテリジェントな自動化のためのGPUアクセラレーション対応ローカルAI言語モデル
  • Qdrant(オプション):高度なAIワークフローと類似検索のためのベクトルデータベース

インフラとモニタリング:

  • Cloudflareトンネル:安全な外部接続
  • Traefik:自動SSLとモニタリングダッシュボード付きリバースプロキシ

全体の仕組み

クライアントが環境にアクセスする際の完全なフローは以下の通りです。

  1. クライアントがカスタムドメイン(例:workflows.client-a.com)にアクセスします
  2. CloudflareトンネルがリクエストをローカルのTraefikインスタンスにルーティングします
  3. Traefikがドメインを読み取り、ミドルウェア(認証、SSL、レート制限)を適用し、正しいクライアントコンテナに転送します
  4. 設定されている場合、AuthentikがすべてのサービスにわたるSSO認証を処理します
  5. クライアントは、自分のデータと設定を持つ完全に分離された環境を利用できます
  6. 他のすべてのクライアントは完全に影響を受けず、アクセスもできません

すべてが整理・分離された状態を維持し、各クライアントはauth.client-a.comdatabase.client-a.combackend.client-a.comなどの独自のサブドメイン構造を持ちます。

セットアップの実践手順

基盤の準備

まず、Docker Desktopのインストールとドメイン管理のセットアップが必要です。簡単なクライアントオンボーディングのために、ワイルドカードDNS構造の設定を推奨します。

# 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

テンプレートシステムの作成

テンプレートベースのアプローチにより、その真価が発揮されます。各クライアントを手動でセットアップする代わりに、クライアント固有の設定で即座にデプロイできるテンプレートを作成します。

包括的なディレクトリ構造を作成します。

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

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

完全なマルチサービステンプレート設定

全16サービスを含む包括的なdocker-compose.ymlテンプレートを作成します。

version: '3.8'

networks:
  ${TENANT_NETWORK}:
    driver: bridge

services:
  # External Connectivity
  cloudflare-tunnel:
    image: cloudflare/cloudflared:latest
    container_name: ${TENANT_PREFIX}-tunnel
    command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TOKEN}
    networks:
      - ${TENANT_NETWORK}
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "cloudflared tunnel info ${TUNNEL_ID} || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Reverse Proxy & Load Balancer
  traefik:
    image: traefik:v3.0
    container_name: ${TENANT_PREFIX}-traefik
    command:
      - "--api.dashboard=true"
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.network=${TENANT_NETWORK}"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=${ADMIN_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "${TRAEFIK_PORT}:80"
      - "${TRAEFIK_SECURE_PORT}:443" 
      - "${TRAEFIK_DASHBOARD_PORT}:8080"
    networks:
      - ${TENANT_NETWORK}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/acme.json:/acme.json
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.${CLIENT_DOMAIN}`)"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
    healthcheck:
      test: ["CMD", "traefik", "healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Database Backend
  postgres:
    image: postgres:15-alpine
    container_name: ${TENANT_PREFIX}-postgres
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_MULTIPLE_DATABASES: n8n,supabase,authentik,nocodb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    networks:
      - ${TENANT_NETWORK}
    ports:
      - "${POSTGRES_PORT}:5432"
    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:latest
    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_NETWORK}
    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}"
    depends_on:
      postgres:
        condition: service_healthy

  # No-Code Database Interface  
  nocodb:
    image: nocodb/nocodb:latest
    container_name: ${TENANT_PREFIX}-nocodb
    environment:
      NC_DB: "pg://postgres:${POSTGRES_PASSWORD}@postgres:5432/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_NETWORK}
    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}"
    depends_on:
      postgres:
        condition: service_healthy

  # Supabase Backend Stack (5 containers)
  supabase-studio:
    image: supabase/studio:latest
    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_NETWORK}
    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}"
    healthcheck:
      disable: true
    depends_on:
      postgres:
        condition: service_healthy

  supabase-meta:
    image: supabase/postgres-meta:latest
    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_NETWORK}
    depends_on:
      postgres:
        condition: service_healthy

  supabase-auth:
    image: supabase/gotrue:latest
    container_name: ${TENANT_PREFIX}-supabase-auth
    environment:
      GOTRUE_API_HOST: 0.0.0.0
      GOTRUE_API_PORT: 9999
      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_NETWORK}
    depends_on:
      postgres:
        condition: service_healthy

  supabase-rest:
    image: postgrest/postgrest:latest
    container_name: ${TENANT_PREFIX}-supabase-rest
    environment:
      PGRST_DB_URI: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/supabase
      PGRST_DB_SCHEMAS: public,graphql_public
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET}
      PGRST_DB_USE_LEGACY_GUCS: "false"
    networks:
      - ${TENANT_NETWORK}
    depends_on:
      postgres:
        condition: service_healthy

  supabase-realtime:
    image: supabase/realtime:latest
    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}
      FLY_ALLOC_ID: fly123
      FLY_APP_NAME: realtime
      SECRET_KEY_BASE: ${SUPABASE_JWT_SECRET}
      ERL_AFLAGS: -proto_dist inet_tcp
      ENABLE_TAILSCALE: "false"
      DNS_NODES: "''"
    networks:
      - ${TENANT_NETWORK}
    command: >
      sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
    depends_on:
      postgres:
        condition: service_healthy

  supabase-kong:
    image: kong:3.2-alpine
    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_NETWORK}
    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}"

  # Local AI Language Models
  ollama:
    image: ollama/ollama:latest
    container_name: ${TENANT_PREFIX}-ollama
    environment:
      OLLAMA_HOST: 0.0.0.0:11434
      OLLAMA_ORIGINS: "*"
    volumes:
      - ollama_data:/root/.ollama
    networks:
      - ${TENANT_NETWORK}
    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}"
    deploy:
      resources:
        limits:
          memory: 16G
        reservations:
          memory: 8G
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Enterprise SSO Authentication (3 containers)
  authentik-redis:
    image: redis:alpine
    container_name: ${TENANT_PREFIX}-authentik-redis
    command: --save 60 1 --loglevel warning
    networks:
      - ${TENANT_NETWORK}
    volumes:
      - authentik_redis_data:/data
    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_NETWORK}
    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"
    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_NETWORK}
    depends_on:
      postgres:
        condition: service_healthy
      authentik-redis:
        condition: service_healthy

  # Test Service for Health Monitoring
  whoami:
    image: traefik/whoami:latest
    container_name: ${TENANT_PREFIX}-whoami
    networks:
      - ${TENANT_NETWORK}
    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"

volumes:
  postgres_data:
  n8n_data:
  nocodb_data:
  ollama_data:
  authentik_redis_data:
  authentik_media:
  authentik_templates:

完全な環境テンプレート

包括的なクライアント固有の変数用に.env.templateを作成します。

# Client Configuration
CLIENT_NAME=CLIENT_NAME_PLACEHOLDER
CLIENT_DOMAIN=CLIENT_DOMAIN_PLACEHOLDER
TENANT_PREFIX=CLIENT_PREFIX_PLACEHOLDER
TENANT_NETWORK=CLIENT_NETWORK_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

# Infrastructure Ports
TRAEFIK_PORT=80
TRAEFIK_SECURE_PORT=443
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=2024.8.3

# n8n Configuration
N8N_PROTOCOL=https
N8N_SECURE_COOKIE=true

# Cloudflare Integration
CLOUDFLARE_TOKEN=CLOUDFLARE_TOKEN_PLACEHOLDER
TUNNEL_ID=TUNNEL_ID_PLACEHOLDER

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

データベース初期化スクリプト

init/01-create-multiple-databases.sqlに包括的なデータベース初期化を作成します。

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

-- Grant permissions
GRANT ALL PRIVILEGES ON DATABASE n8n TO postgres;
GRANT ALL PRIVILEGES ON DATABASE nocodb TO postgres;
GRANT ALL PRIVILEGES ON DATABASE supabase TO postgres;
GRANT ALL PRIVILEGES ON DATABASE authentik TO postgres;

-- Enable required extensions for Supabase
\c supabase;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "pgjwt";

-- 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';

Supabase Kongゲートウェイ設定

APIゲートウェイルーティング用にsupabase/kong.ymlを作成します。

_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

自動デプロイスクリプト

数分で新しいクライアント環境を作成する包括的なデプロイスクリプト:

#!/bin/bash
# deploy-client.sh - Complete Multi-Tenant Deployment

CLIENT_DOMAIN=$1
CLIENT_NAME=$2
CLOUDFLARE_TOKEN=$3

if [ -z "$CLIENT_DOMAIN" ] || [ -z "$CLIENT_NAME" ] || [ -z "$CLOUDFLARE_TOKEN" ]; then
    echo "Usage: ./deploy-client.sh example.com 'Client Name' 'cloudflare-token'"
    echo ""
    echo "Example: ./deploy-client.sh client-a.com 'Client A Corporation' 'your-cloudflare-token'"
    exit 1
fi

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/" 2>/dev/null || true

echo "Copied configuration templates"

# Generate secure passwords and secrets
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25)
NOCODB_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16)
SUPABASE_JWT_SECRET=$(openssl rand -base64 64 | tr -d "=+/" | cut -c1-64)
AUTHENTIK_SECRET=$(openssl rand -hex 32)

echo "Generated secure credentials"

# Calculate unique ports to avoid conflicts
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
cat .env.template | \
    sed "s/CLIENT_NAME_PLACEHOLDER/$CLIENT_NAME/g" | \
    sed "s/CLIENT_DOMAIN_PLACEHOLDER/$CLIENT_DOMAIN/g" | \
    sed "s/CLIENT_PREFIX_PLACEHOLDER/$CLIENT_PREFIX/g" | \
    sed "s/CLIENT_NETWORK_PLACEHOLDER/${CLIENT_PREFIX}-network/g" | \
    sed "s/SECURE_PASSWORD_PLACEHOLDER/$POSTGRES_PASSWORD/g" | \
    sed "s/NOCODB_PASSWORD_PLACEHOLDER/$NOCODB_PASSWORD/g" | \
    sed "s/SUPABASE_JWT_PLACEHOLDER/$SUPABASE_JWT_SECRET/g" | \
    sed "s/AUTHENTIK_SECRET_PLACEHOLDER/$AUTHENTIK_SECRET/g" | \
    sed "s/CLOUDFLARE_TOKEN_PLACEHOLDER/$CLOUDFLARE_TOKEN/g" | \
    sed "s/8080/$TRAEFIK_DASHBOARD_PORT/g" | \
    sed "s/5432/$POSTGRES_PORT/g" \
    > "$DEPLOY_DIR/.env"

echo "Generated environment configuration"

# Create ACE 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 core infrastructure first
docker-compose up -d cloudflare-tunnel traefik postgres

echo "Waiting for database to be ready..."
sleep 30

# Start all remaining services
docker-compose up -d

echo ""
echo "Multi-tenant environment deployed successfully!"
echo ""
echo "Access URLs:"
echo "   Traefik Dashboard: http://localhost:$TRAEFIK_DASHBOARD_PORT"
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 (save these securely):"
echo "   Client: $CLIENT_NAME"
echo "   PostgreSQL Password: $POSTGRES_PASSWORD"
echo "   NocoDB Admin Password: $NOCODB_PASSWORD"
echo "   Supabase JWT Secret: [hidden - check .env file]"
echo ""
echo "Next Steps:"
echo "   1. Configure Cloudflare DNS: *.${CLIENT_DOMAIN} -> tunnel"
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"

マルチテナントスタックの使い方

新規クライアントのデプロイ

包括的なスクリプトにより、新しいクライアント環境の作成が簡単になります。

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

各デプロイにより以下が作成されます。

  • 16コンテナを持つ完全に分離されたDockerネットワーク
  • 永続ストレージ用の個別データボリューム
  • ヘルスモニタリング付きの固有サービスコンテナ
  • 個別のCloudflareトンネル設定
  • SSL証明書付きのカスタムドメインルーティング
  • アクティベーション可能なエンタープライズグレードのSSOインフラ

複数環境の管理

中央の場所からすべてのクライアント環境を監視します。

# 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 latest images
docker-compose pull && docker-compose up -d

コンテナアーキテクチャの詳細

クライアントごとの完全な16コンテナアーキテクチャには以下が含まれます。

インフラストラクチャレイヤー(4コンテナ):

  • cloudflare-tunnel:安全な外部接続
  • traefik:自動SSLとサービスディスカバリ付きリバースプロキシ
  • postgres:コネクションプーリング付き中央データベース
  • whoami:ヘルスモニタリングとルーティング検証

アプリケーションレイヤー(7コンテナ):

  • n8n:PostgreSQLバックエンド付きワークフロー自動化
  • nocodb:ノーコードデータベースインターフェース
  • supabase-studio:バックエンド開発ダッシュボード
  • supabase-meta:データベースイントロスペクションサービス
  • supabase-auth:認証とユーザー管理
  • supabase-rest:自動生成REST API
  • supabase-realtime:リアルタイムサブスクリプションと更新

AIとゲートウェイレイヤー(2コンテナ):

  • ollama:GPUアクセラレーション対応ローカルAI
  • supabase-kong:レート制限とCORS付きAPIゲートウェイ

エンタープライズセキュリティレイヤー(3コンテナ):

  • authentik-server:SSO認証サーバー
  • authentik-worker:バックグラウンドタスクと通知
  • authentik-redis:セッション管理とキャッシング

クライアントごとのリソーススケーリング

クライアントのニーズと使用パターンに基づいてリソースを調整します。

# High-performance client configuration
services:
  n8n:
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 8G
        reservations:
          cpus: '2.0'
          memory: 4G
  
  postgres:
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G
    environment:
      - POSTGRES_MAX_CONNECTIONS=200
      - POSTGRES_SHARED_BUFFERS=1GB
      - POSTGRES_EFFECTIVE_CACHE_SIZE=3GB

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

ビジネスへの実際のメリット

エンタープライズ機能を備えた完全なクライアント分離

各クライアントは、エンタープライズグレードのSSO、AI機能、完全なバックエンドインフラを含む独自の包括的な環境を得ます。データ、設定、カスタマイズ、セキュリティポリシーは完全に封じ込められます。あるクライアントの問題が他のクライアントに影響することはありません。個別のn8nデプロイで実現する分離と同様のコンセプトです。

フル機能セットによる迅速なクライアントオンボーディング

新規クライアントは、完全な開発・自動化スタックを10分以内で稼働させることができます。デプロイスクリプトが、複雑な設定、DNSセットアップ、サービスの初期化、セキュリティ設定をすべて自動的に処理します。従来のアプローチよりもはるかに包括的です。

予測可能なエンタープライズコスト

初期セットアップ後は、基本インフラ以外にクライアントごとのホスティングコストは発生しません。シートごと、ワークフローごと、またはAPIコールごとに課金するSaaSソリューションとは異なり、ハードウェアに一度投資するだけで、フルエンタープライズ機能を備えた無制限のクライアント環境を運用できます。

プロフェッショナルなブランドの一貫性

各クライアントは、プロフェッショナルなサブドメイン(workflows.client.comauth.client.comなど)を持つ独自のブランドドメインを取得し、環境を完全にカスタマイズできます。ブランドアイデンティティを希薄化する“Powered by”フッターや共有インターフェースはありません。

n8n統合:大規模なエンタープライズワークフロー自動化

ここからが真に強力な部分です。n8nをセルフホストしてワークフロー自動化を実現する方法をご紹介してきましたが、このマルチテナントセットアップにより、各クライアントが完全なエンタープライズスタックと統合された独自のn8nインスタンスを持つことができます。

各クライアントは以下のような高度なワークフローを構築できます。

  • 独自のデータベース(NocoDB、Supabase PostgreSQL)への接続
  • 独自のAIモデル(Ollama)を使用したインテリジェントな自動化
  • エンタープライズSSO(Authentik)による認証
  • 特定のビジネスツールやAPIとの統合
  • 完全な分離とセキュリティの下でのデータ処理

この組み合わせにより、以下を実現する強力なクライアントデリバリープラットフォームが生まれます。

  • 標準化された自動化機能の迅速なデプロイ
  • 他のクライアントに影響を与えないクライアントごとのワークフローカスタマイズ
  • コストの線形増加なしにサービスデリバリーをスケール
  • 各クライアントの完全なデータ主権の維持
  • エンタープライズグレードのセキュリティとコンプライアンスの提供

このアプローチは、WindmillのDockerセットアップガイドで使用したのと同じ原則に基づいていますが、完全なマルチテナントアーキテクチャに拡張しています。

高度な設定オプション

AuthentikによるエンタープライズSSOの実装

Authentikのフォワード認証を設定して、すべてのクライアントサービスにわたるシングルサインオンを有効にします。

# Add to Traefik middleware configuration
middlewares:
  auth-global:
    forwardAuth:
      address: "http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
      trustForwardHeader: true
      authResponseHeaders:
        - X-authentik-username
        - X-authentik-groups
        - X-authentik-email
        - X-authentik-name
        - X-authentik-uid

次に、ミドルウェアを使用するようにサービスのラベルを更新します。

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

高度なAI向けベクトルデータベースの追加

Qdrantベクトルデータベースでのアクティブ機能の強化:

qdrant:
  image: qdrant/qdrant:latest
  container_name: ${TENANT_PREFIX}-qdrant
  environment:
    QDRANT__SERVICE__HTTP_PORT: 6333
    QDRANT__SERVICE__GRPC_PORT: 6334
  volumes:
    - qdrant_data:/qdrant/storage
  networks:
    - ${TENANT_NETWORK}
  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}"

ハイブリッドAIアーキテクチャの実装

最適なパフォーマンスのために、コンテナ化されたAIとネイティブAIを組み合わせたハイブリッドアプローチを検討してください。

# 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

これにより、他のサービスのコンテナ分離を維持しながら、直接GPU アクセスによる5〜6倍のパフォーマンス向上が実現できます。

モニタリングと可観測性スタック

クライアントごとに包括的なモニタリングを追加します。

prometheus:
  image: prom/prometheus:latest
  container_name: ${TENANT_PREFIX}-prometheus
  volumes:
    - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
    - prometheus_data:/prometheus
  networks:
    - ${TENANT_NETWORK}
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.prometheus.rule=Host(`metrics.${CLIENT_DOMAIN}`)"

grafana:
  image: grafana/grafana:latest
  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_NETWORK}
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.grafana.rule=Host(`monitoring.${CLIENT_DOMAIN}`)"
    - "traefik.http.routers.grafana.middlewares=${AUTH_MIDDLEWARE}"

よくある問題と解決策

“Service Unavailable”またはHTTP 502エラー

通常、Traefikがターゲットコンテナに到達できないことを意味します。以下を確認してください。

# 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

DNS解決の問題

ワイルドカードDNSの設定は、サブドメインルーティングにとって非常に重要です。

# 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

複数クライアント間でのリソース枯渇

すべてのクライアント環境のリソース使用状況を監視します。

# 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-compose -f deployments/*/docker-compose.yml ps --format "table {{.Name}}\t{{.Size}}"

データベースコネクションプールの枯渇

多数のクライアントがいる場合、PostgreSQLのコネクション制限に達する可能性があります。デプロイごとに設定してください。

-- 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';

-- Reload configuration
SELECT pg_reload_conf();

Authentik SSO設定の問題

よくあるSSO設定の問題と解決策:

# 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

# Reset Authentik admin password if needed
docker-compose exec authentik-server ak create_admin_group
docker-compose exec authentik-server ak bootstrap_tasks

Cloudflareトンネル接続の問題

トンネル接続の問題をデバッグします。

# 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

インフラストラクチャの考慮事項

複数クライアント向けインフラのサイジング

フル16コンテナスタックで10〜15クライアントを同時に処理する一般的なセットアップの場合:

最小要件:

  • CPU:16〜24コア(アクティブなクライアント環境あたり2コア)
  • RAM:64〜128GB(AI使用状況に応じてクライアントあたり4〜8GB)
  • ストレージ:2TB以上のNVMe SSD(データベース、AIモデル、ログは時間とともに増大)
  • ネットワーク:レスポンシブなクライアントアクセスのためのギガビット接続

本番環境の推奨事項:

  • サーバー:Hetzner CCX62または同等品(48 vCPU、192GB RAM)
  • ストレージ:自動バックアップシステム付き4TB NVMe
  • ネットワーク:複数の冗長接続
  • モニタリング:アラート付き完全な可観測性スタック

マルチクライアント環境のバックアップ戦略

クライアントごとの自動バックアップを実装します。

#!/bin/bash
# backup-all-clients.sh - Comprehensive backup solution
BACKUP_DIR="/opt/backups"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)

for client_dir in deployments/*/; do
    if [ -d "$client_dir" ]; then
        CLIENT_DOMAIN=$(basename "$client_dir")
        echo "Backing up client: $CLIENT_DOMAIN"
        
        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 "${PWD}":/backup \
            -v "${CLIENT_DOMAIN//.}_n8n_data":/data/n8n:ro \
            -v "${CLIENT_DOMAIN//.}_nocodb_data":/data/nocodb:ro \
            -v "${CLIENT_DOMAIN//.}_ollama_data":/data/ollama:ro \
            alpine tar czf "/backup/${BACKUP_DIR}/${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/
        
        echo "Backup completed for $CLIENT_DOMAIN"
    fi
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/

本番環境向けセキュリティ強化

包括的なセキュリティベストプラクティスを実装します。

# Enhanced Traefik security configuration
traefik:
  command:
    - "--api.dashboard=true"
    - "--api.debug=false"
    - "--log.level=WARN"
    - "--accesslog=true"
    - "--entrypoints.web.address=:80"
    - "--entrypoints.websecure.address=:443"
    - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
    - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
    - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
    - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    - "--providers.docker.exposedbydefault=false"
  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"

セキュリティミドルウェアをすべてのクライアントサービスに適用します。

labels:
  - "traefik.http.routers.n8n.middlewares=security,ratelimit,${AUTH_MIDDLEWARE}"

コスト分析:重要な数字

従来のSaaSコスト(フル機能セットの10エンタープライズクライアント)

クライアントあたりの月額コスト:

  • n8n Pro:$50/月/クライアント = $500/月
  • Supabase Pro:$25/月/クライアント = $250/月
  • ノーコードプラットフォーム(Airtable):$20/月/クライアント = $200/月
  • エンタープライズSSO(Auth0):$23/月/クライアント = $230/月
  • AI APIコスト(OpenAI):$50/月/クライアント = $500/月
  • 合計:$1,500/月 = $18,000/年

セルフホストマルチテナントエンタープライズスタックのコスト

年間インフラコスト:

  • 専用サーバー(Hetzner CCX62):$350/月 = $4,200/年
  • ドメインコスト(10クライアント):$120/年
  • Cloudflare Pro(オプション):$240/年
  • 合計:$4,560/年

年間節約額:$13,440(75%のコスト削減)

さらに以下が得られます。

  • 完全なデータ主権とプライバシー
  • 無制限のカスタマイズとホワイトラベリング
  • ベンダーロックインやAPIレート制限なし
  • エンタープライズグレードのセキュリティとコンプライアンス
  • リセラーサービスの提供能力
  • アップデートと機能の完全なコントロール

このセットアップが通常SaaSサブスクリプションでははるかに高額になるエンタープライズ機能を提供することを考えると、特に強力です。n8nのセルフホスティング分析で実証したコストメリットと同様のコンセプトです。

WordPress統合:コンテンツワークフローの効率化

開発環境と並行してWordPressサイトを管理するエージェンシーやチームにとって、このマルチテナントスタックはWordPress自動化ワークフローと見事に統合できます。tva Duplicate ProプラグインがWordPressでのコンテンツ管理を効率化するのと同様に、このコンテナ化された環境はWordPressサイトと開発インフラ間の複雑なワークフローを自動化できます。

WordPress統合の可能性:

  • コンテンツシンジケーション:WordPressコンテンツをクライアントシステムに自動的にプッシュするn8nワークフロー
  • 自動デプロイ:WordPressサイトの変更がクライアント環境でのデプロイをトリガー
  • データ同期:クライアントのデータベース変更(NocoDB経由)がWordPressコンテンツを自動更新
  • AI活用コンテンツ:OllamaモデルがコンテンツをWordPressサイトに生成・配信
  • クライアントレポーティング:開発環境のメトリクスから自動生成されるWordPressレポート

これにより、WordPressコンテンツ管理、開発ワークフロー、クライアントデリバリーがすべてシームレスに連携する包括的なエコシステムが構築されます。

セットアップに費やす時間に見合う価値はあるか?

複数クライアントの開発環境を管理している方、SaaSビジネスを構築している方、技術ソリューションを提供するエージェンシーを運営している方なら、間違いなく価値があります。初期セットアップには約1日かかりますが、最終的に以下が得られます。

即座のメリット:

  • 自動クライアントオンボーディング – 完全なエンタープライズスタックを10分以内で構築
  • 完全な分離 – プロフェッショナルなブランディングを備えたクライアント環境間の完全分離
  • 大幅なコスト削減 – マネージドサービスと比較して75%以上のコスト削減
  • 完全なコントロール – データ、カスタマイズ、コンプライアンスの完全な管理
  • スケーラブルなアーキテクチャ – ビジネスの成長に合わせて拡張可能
  • エンタープライズグレードのセキュリティ – SSOと認証を完備

長期的な価値:

  • クライアントリテンション – 優れたサービスデリバリーとプロフェッショナルなプレゼンテーションによる維持
  • 収益の成長 – より多くのクライアントを効率的にサービスする能力
  • 競争優位性 – 競争力のある価格でエンタープライズ機能を提供
  • 技術的専門性 – 市場での差別化

複数クライアントにサービスを提供するエージェンシーや、コントロールを維持しながらスケールしたいSaaSスタートアップにとって、このセットアップはエンタープライズグレードのコストなしにエンタープライズグレードの機能を提供します。コンテナ化、自動デプロイ、適切なドメインルーティング、エンタープライズセキュリティの組み合わせにより、完全にコントロール可能な本格的なビジネス成長の基盤が生まれます。

このセットアップは、WordPress自動化などの既存ツールとの統合の可能性と、セルフホスト自動化プラットフォームの実証済みの安定性を考慮すると、さらに価値が高まります。

今後の展望

このマルチテナントアーキテクチャの強化に積極的に取り組んでいます。今後のチュートリアルでは以下をカバーする予定です。

高度なデプロイオプション:

  • Kubernetes移行ガイド – 究極のスケーラビリティとエンタープライズデプロイ向け
  • 自動SSL証明書管理 – 統合されたLet’s Encryptワークフロー
  • 高度なモニタリングとアラート – Prometheus、Grafana、カスタムダッシュボード
  • 災害復旧の自動化 – マルチリージョンバックアップ戦略

クライアントエクスペリエンスの強化:

  • クライアントセルフサービスポータル – 自身の環境と設定の管理
  • ホワイトラベルカスタマイズ – エージェンシーブランディング用テンプレート
  • 高度なワークフローテンプレート – 一般的なビジネスプロセス向け
  • 統合ガイド – 人気のビジネスツールやAPI向け

エンタープライズ機能:

  • 高度なセキュリティ強化 – WAFと侵入検知
  • コンプライアンスフレームワーク – GDPR、SOC2、その他の規制対応
  • マルチリージョンデプロイ – グローバルなクライアントベース向け戦略
  • パフォーマンス最適化 – 高トラフィック環境向けガイド

クライアントサービスデリバリーの未来は、コントロールと利便性のどちらかを選ぶことではありません。効率的にスケールしながらプロフェッショナルな基準を維持し、その両方を実現するシステムを構築することです。

プロフェッショナルサポートのご案内

16のコンテナ化されたサービスを持つマルチテナント環境のセットアップには、多くの可動部分が関わります。包括的なドキュメントを提供していますが、ビジネスごとに固有の要件や既存のインフラに関する考慮事項があります。

本番環境でこのセットアップを導入する場合や、特定のクライアントデリバリーニーズに合わせたカスタマイズが必要な場合は、以下のサポートを提供いたします。

  • インフラに合わせたカスタムデプロイ戦略
  • 既存のシステムおよびワークフローとの統合
  • 特定のクライアント負荷に対するパフォーマンス最適化
  • コンプライアンス要件のためのセキュリティ強化
  • マルチテナント環境管理に関するスタッフトレーニング
  • 継続的なメンテナンスとモニタリング戦略

マルチテナントアーキテクチャのニーズについてご相談いただき、導入に関するプロフェッショナルなガイダンスをご希望の場合は、tva.sgからお問い合わせください。

既存のエージェンシーの拡大、新しいSaaSプラットフォームの立ち上げ、エンタープライズグレードのクライアントデリバリー能力の構築のいずれであっても、独立性を維持しながらプロフェッショナルな成果を提供するセルフホスト型コンテナ化ソリューションの実現を支援いたします。