使用 Docker 构建多租户开发栈:可扩展客户部署的完整配置
如何创建基于模板的多租户开发环境,包含 16 个容器化服务,支持离线开发,也可通过基于子域名的路由提供在线访问。
管理多个客户的开发环境,往往意味着要在复杂的手动配置与昂贵的云解决方案之间做选择。手动部署耗时且容易出错。云平台虽然方便,却会带来供应商锁定,以及随用量增长的持续费用。
今天,我们将逐步搭建一个可扩展的多租户开发栈,兼顾客户环境之间的可靠隔离与自动化部署,并让您始终掌握基础设施的完整控制权。这一方案延续了我们的自托管理念:正如我们介绍过的自托管 n8n 实现工作流自动化和使用 Docker 部署 Windmill,目的都是全面掌控系统运维。
我们使用的工具
先来了解这套完整的 16 容器架构中各组件的作用:
Docker:容器化基础
Docker 为多租户环境提供所需的隔离性和一致性。每个客户都有独立的容器,采用一致的配置,有助于确保开发环境中可用的配置在生产环境中同样可用。您可以将其理解为:在同一套硬件上运行多台独立的服务器,但它们仍共享 CPU、内存、磁盘和内核。
核心优势在于客户之间的可靠隔离。每个客户的数据、配置和定制内容分别存放在独立的容器、网络和卷中,因此不会干扰其他客户。在为需求和安全要求各不相同的多个企业客户提供服务时,这一点至关重要。
Traefik:智能反向代理与负载均衡器
Traefik 负责智能调度流量,根据域名自动将请求路由到相应的客户环境。无需手动编写复杂的 Apache 或 Nginx 规则,Traefik 会读取 Docker 容器标签,自动配置路由。
可以把 Traefik 看作一位熟悉各个办公室的接待员:每位访客(请求)该进入哪个办公室(容器),它都清楚,无需您每次指路。在本方案中,Traefik 负责 SSL 终止、自动服务发现,并提供详细的监控仪表盘。
Cloudflare Tunnels:安全的外部访问
Cloudflare Tunnels 无需复杂的防火墙配置或 VPN,即可让外部安全访问本地开发栈。每个客户域名都有独立的隧道,从而将各客户的外部访问路径分开。
这样,开发环境仍安全地运行在本地,客户却可以在通过适当身份验证后,从任何地方访问自己的服务。这与我们在n8n 托管指南中配置安全外部访问的方式类似。
完整服务栈:满足客户所需
我们的多租户栈涵盖四类核心服务,每个客户配置 16 个容器:
工作流自动化与业务逻辑:
- n8n:用于业务流程自动化的完整工作流自动化平台
- Authentik:企业级单点登录与身份管理(3 个容器:服务器、工作进程和 Redis 缓存)
数据库与后端服务:
- PostgreSQL:为所有服务提供可靠的数据库后端,并优化连接池
- Supabase 服务栈:完整的后端即服务,由 6 个专用容器组成(Studio、Meta、Auth、REST API、Realtime、Kong Gateway)
- NocoDB:用于客户数据管理的无代码数据库界面
AI 与智能功能:
- Ollama:支持 GPU 加速的本地 AI 语言模型,用于智能自动化
- Qdrant(可选):用于高级 AI 工作流和相似性搜索的向量数据库
基础设施与监控:
- Cloudflare Tunnel:安全的外部连接
- Traefik:提供自动 SSL 配置和监控仪表盘的反向代理
各组件如何协同工作
客户访问其环境时,完整流程如下:
- 客户访问自己的定制域名,例如
workflows.client-a.com - Cloudflare Tunnel 将请求路由到本地 Traefik 实例
- Traefik 读取域名,应用中间件(身份验证、SSL、速率限制),并转发到相应的客户容器
- 如果已配置 Authentik,则由它负责所有服务的 SSO 身份验证
- 客户进入隔离的专属环境,使用自己的数据和配置
- 无法通过该客户的路由访问其他客户的环境
所有环境都井然有序、彼此隔离,每个客户都有自己的子域名结构,例如 auth.client-a.com、database.client-a.com、backend.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 模板。这是一个开发模板:Traefik 仪表盘在没有身份验证的情况下运行,因此模板只在 127.0.0.1 上发布它和 PostgreSQL,其他机器无法访问(参见下文“生产环境安全加固”):
version: '3.8'
networks:
default:
name: ${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:
- default
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"
- "127.0.0.1:${TRAEFIK_DASHBOARD_PORT}:8080"
networks:
- default
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/acme.json:/acme.json
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:
- default
ports:
- "127.0.0.1:${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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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:
- default
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";
-- 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 网关配置
创建 supabase/kong.yml,配置 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
自动化部署脚本
使用此部署脚本可在几分钟内创建新客户环境。它根据客户前缀的校验和计算所有主机端口(Traefik 的 HTTP、HTTPS 和仪表盘,以及 PostgreSQL),因此多个客户可以共用一台主机;请检查两个客户的偏移量是否冲突:
#!/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))
TRAEFIK_PORT=$((10000 + PORT_OFFSET))
TRAEFIK_SECURE_PORT=$((11000 + 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/^TRAEFIK_PORT=80$/TRAEFIK_PORT=$TRAEFIK_PORT/" | \
sed "s/^TRAEFIK_SECURE_PORT=443$/TRAEFIK_SECURE_PORT=$TRAEFIK_SECURE_PORT/" | \
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 APIsupabase-realtime:实时订阅与更新
AI 与网关层(2 个容器):
ollama:支持 GPU 加速的本地 AIsupabase-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 功能和完整的后端基础设施。数据、配置、定制内容与安全策略保持分离。某个客户环境中的应用层问题会限定在该环境内,但由于所有客户共用一台主机,CPU、内存或磁盘资源耗尽等主机层问题仍可能影响所有客户。这与我们通过独立部署 n8n 实现的隔离。
快速接入客户,提供完整功能
新客户可以在不到 10 分钟内启用完整的开发与自动化栈。部署脚本自动完成配置、凭据生成和服务初始化;Cloudflare DNS 记录和可选的 SSO 仍需通过简短的手动步骤设置,具体可见脚本末尾的说明。
可预测的企业级运行成本
完成初始配置后,除了基础设施本身的费用,不再产生按客户收取的托管费用。SaaS 方案往往按席位、工作流或 API 调用收费,而这里您支付固定的服务器费用,即可在其资源允许的范围内运行具备完整企业级功能的客户环境。
专业且一致的品牌呈现
每个客户都有体现自身品牌的域名和专业的子域名(如 workflows.client.com、auth.client.com 等),并可全面定制自己的环境。不会出现削弱品牌辨识度的“由某平台提供支持”页脚或共享界面。
n8n 集成:规模化的企业工作流自动化
这里的能力尤为突出。正如我们介绍过如何自托管 n8n 实现工作流自动化,这套多租户方案为每个客户提供独立、完整的 n8n 实例,并将其与完整的企业级服务栈集成。
每个客户都可以构建复杂的工作流,用于:
- 连接自己的数据库(NocoDB、Supabase PostgreSQL)
- 使用自己的 AI 模型(Ollama)实现智能自动化
- 通过企业级 SSO(Authentik)完成身份验证
- 集成各自的业务工具和 API
- 在各自隔离的环境中处理自己的数据
这一组合构成了功能强大的客户交付平台,让您能够:
- 快速部署标准化的自动化功能
- 为每个客户定制工作流,且不影响其他客户
- 扩大服务交付规模,避免成本线性增长
- 为每个客户保留完整的数据主权
- 提供基于 SSO 的访问控制,作为安全与合规工作的基础
这一方案沿用了我们在 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 向量数据库增强 AI 功能:
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:
- default
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
在 macOS 上,这一点很重要,因为 Docker Desktop 不支持在容器中使用 GPU,而原生运行的 Ollama 可以直接利用 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:
- default
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:
- default
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 Tunnel 连接问题
排查隧道连接问题:
# Check tunnel status
docker-compose logs cloudflare-tunnel --tail=20
# Verify tunnel configuration in Cloudflare dashboard
# Ensure wildcard routing: *.client-domain.com
# Test tunnel connectivity
curl -I https://test.client-domain.com
基础设施注意事项
为多个客户规划基础设施容量
典型配置需要同时为 10-15 个客户运行完整的 16 容器栈:
最低要求:
- CPU:16-24 核(每个活跃客户环境分配 2 核)
- RAM:64-128GB(每个客户 4-8GB,取决于 AI 使用情况)
- 存储: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)
STACK_DIR=$(pwd) # run from the development-stack directory
mkdir -p "$BACKUP_DIR"
for client_dir in "$STACK_DIR"/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 "${BACKUP_DIR}":/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/${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,680/月 = $20,160/年
自托管多租户企业级服务栈成本
年度基础设施费用:
- 配备专用 vCPU 的云服务器(Hetzner CCX62):$350/月 = $4,200/年
- 域名费用(10 个客户):$120/年
- Cloudflare Pro(可选):$240/年
- 合计:$4,560/年
每年节省:$15,600(成本降低 77%)
这些数字反映的是本指南于 2025 年 8 月发布时的价格。截至 2026 年 10 月,Auth0 的 B2C Essentials 套餐起价为 $35/月,Hetzner 已不再列出 CCX62;其后继型号 CCX63 配备 48 vCPU 和 192 GB 内存,标价为 €853.99/月,不含增值税,包含一个主 IPv4 地址。决定前,请用当前价格重新计算两种方案的费用。
此外,您还可以获得:
- 完整的数据主权与隐私保护
- 不受限制的定制与白标能力
- 无供应商锁定或 SaaS 套餐的速率限制
- 由您自行管理的安全与合规控制措施
- 提供转售服务的能力
- 对更新和功能的完整控制权
这套配置提供的企业级功能,如果通过 SaaS 订阅获取,通常需要支付高得多的费用。这一点尤其有价值,也与我们在 n8n 自托管成本分析中展示的成本优势类似。
WordPress 集成:简化内容工作流
对于同时管理 WordPress 网站和开发环境的代理服务商与团队,这套多租户栈可以很好地融入 WordPress 自动化工作流。正如 tva Duplicate Pro 插件简化了 WordPress 内容管理,这套容器化环境也可以自动执行 WordPress 网站与开发基础设施之间的复杂工作流。
WordPress 集成的应用方向:
- 内容分发:通过 n8n 工作流自动将 WordPress 内容推送到客户系统
- 自动化部署:WordPress 网站变更触发客户环境中的部署
- 数据同步:客户数据库变更(通过 NocoDB)自动更新 WordPress 内容
- AI 内容生成:使用 Ollama 模型生成内容,并将其传送到 WordPress 网站
- 客户报告:根据开发环境指标自动生成 WordPress 报告
由此构成一个完整的生态系统,使 WordPress 内容管理、开发工作流和客户交付顺畅协同。
值得投入配置时间吗?
如果您需要管理多个客户的开发环境、建立 SaaS 业务,或运营一家交付技术解决方案的代理服务商,答案是肯定的。初始配置约需一天,但您将获得:
即时收益:
- 自动化客户接入:不到 10 分钟即可为客户提供完整的企业级服务栈
- 可靠隔离:客户环境彼此隔离,同时呈现专业的品牌形象
- 大幅节省成本:相比托管服务,成本降低 75%+
- 完整控制权:掌控数据、定制内容与合规事项
- 可扩展架构:随业务增长而扩展
- 基于 SSO 的访问控制:使用 Authentik
长期价值:
- 客户留存:以优质的服务交付和专业形象留住客户
- 收入增长:更高效地服务更多客户
- 竞争优势:以有竞争力的价格提供企业级功能
- 技术专长:让您在市场中脱颖而出
对于服务多个客户的代理服务商,以及希望在扩展规模时保持控制权的 SaaS 初创企业,这套配置可以提供企业级能力,而无需承担企业级成本。容器化、自动化部署、妥善配置的域名路由和企业级安全性相结合,为业务持续增长打下基础,并始终由您完全掌控。
如果再考虑与 WordPress 自动化等现有工具的集成潜力,以及自托管自动化平台经过验证的稳定性,这套配置的价值会更加突出。
下一步是什么?
我们正积极改进这套多租户架构。后续教程将介绍:
高级部署选项:
- Kubernetes 迁移指南:实现更高的可扩展性与企业级部署
- 自动化 SSL 证书管理:集成 Let's Encrypt 工作流
- 高级监控与告警:使用 Prometheus、Grafana 和定制仪表盘
- 自动化灾难恢复:采用多区域备份策略
客户体验改进:
- 客户自助服务门户:让客户管理自己的环境和设置
- 白标定制:提供适用于代理服务商品牌的模板
- 高级工作流模板:覆盖常见业务流程
- 集成指南:对接常用业务工具和 API
企业级功能:
- 高级安全加固:配备 WAF 与入侵检测
- 合规框架:涵盖 GDPR、SOC2 及其他法规
- 多区域部署:面向全球客户群的部署策略
- 性能优化:适用于高流量环境的指南
未来的客户服务交付,无需在控制权与便利性之间二选一,而是要构建兼顾两者的系统,同时高效扩展规模并保持专业标准。
获取专业支持
搭建包含 16 个容器化服务的多租户环境,涉及众多相互关联的组件。虽然我们提供了完整的文档,但每家企业都有独特的需求,也需要考虑现有基础设施。
如果您计划将这套配置用于生产环境,或需要根据特定的客户交付需求进行定制,我们的团队可以提供以下帮助:
- 根据您的基础设施定制部署策略
- 与现有系统和工作流集成
- 针对您的客户负载优化性能
- 根据合规要求进行安全加固
- 为员工提供多租户环境管理培训
- 制定持续维护与监控策略
请通过 tva.sg 联系我们,讨论您的多租户架构需求,并获取专业的实施指导。
无论您是在扩大现有代理服务商的业务、推出新的 SaaS 平台,还是建立企业级客户交付能力,我们都可以帮助您通过自托管的容器化方案实现目标,在保持独立性的同时交付专业成果。