根目录 docker-compose.yml 现在就是一台机器跑完整栈:PostgreSQL、Redis、 迁移、API、worker 和前端 nginx 反代(web),API 默认 TRUST_PROXY=1; 全栈测试专用的 mock 供应商与第二站点入口拆到 docker-compose.fullstack.yml, 由 scripts/run-fullstack-tests.mjs 通过 COMPOSE_FILE 叠加。前端镜像接受 VITE_ 构建参数;README 部署章节改为单机步骤与 Caddy HTTPS 示例。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
214 lines
9.5 KiB
YAML
214 lines
9.5 KiB
YAML
# Lindyson AI 单机部署:一台机器跑完整个栈。
|
|
# docker compose up -d --build
|
|
# 变量从同目录 .env 读取(见 .env.example)。公网只需开放 web 的 80/443;
|
|
# API 只绑定本机回环地址,TLS 在前面加 Caddy 或在 web 容器挂证书即可。
|
|
# 全栈测试用 docker-compose.fullstack.yml 叠加(由 scripts/run-fullstack-tests.mjs 自动加载)。
|
|
|
|
x-api-env: &api-env
|
|
NODE_ENV: ${NODE_ENV:-production}
|
|
HOST: ${HOST:-0.0.0.0}
|
|
PORT: ${PORT:-4300}
|
|
DATABASE_URL: ${COMPOSE_DATABASE_URL:-postgres://lindyson:${POSTGRES_PASSWORD:-lindyson}@postgres:5432/lindyson}
|
|
REDIS_URL: ${COMPOSE_REDIS_URL:-redis://redis:6379}
|
|
# 反向代理只有 web 容器这一跳;如果前面还有别的代理,按跳数或地址列表调整。
|
|
TRUST_PROXY: ${TRUST_PROXY:-1}
|
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set to a strong random value}
|
|
CORS_ORIGIN: ${CORS_ORIGIN:?CORS_ORIGIN must be set to explicit HTTPS origins}
|
|
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:?PUBLIC_BASE_URL must be set to the public HTTPS API origin}
|
|
APP_BASE_URL: ${APP_BASE_URL:?APP_BASE_URL must be set to the public HTTPS app origin}
|
|
MCP_ENABLED: ${MCP_ENABLED:-false}
|
|
MCP_PUBLIC_URL: ${MCP_PUBLIC_URL:-}
|
|
MCP_OAUTH_SECRET: ${MCP_OAUTH_SECRET:-}
|
|
MCP_OAUTH_JWKS_JSON: ${MCP_OAUTH_JWKS_JSON:-}
|
|
MCP_FILE_ENCRYPTION_KEY: ${MCP_FILE_ENCRYPTION_KEY:-}
|
|
MCP_READ_RATE_PER_MINUTE: ${MCP_READ_RATE_PER_MINUTE:-60}
|
|
MCP_GENERATE_RATE_PER_MINUTE: ${MCP_GENERATE_RATE_PER_MINUTE:-6}
|
|
MCP_IMPORT_USER_CONCURRENCY: ${MCP_IMPORT_USER_CONCURRENCY:-2}
|
|
MCP_IMPORT_GLOBAL_CONCURRENCY: ${MCP_IMPORT_GLOBAL_CONCURRENCY:-4}
|
|
STORAGE_DRIVER: ${STORAGE_DRIVER:-local}
|
|
# API 与 worker 是独立容器,必须共同读写挂载卷的绝对路径。
|
|
LOCAL_STORAGE_DIR: /app/storage
|
|
TOS_ENDPOINT: ${TOS_ENDPOINT:-}
|
|
TOS_REGION: ${TOS_REGION:-cn-beijing}
|
|
TOS_BUCKET: ${TOS_BUCKET:-}
|
|
TOS_ACCESS_KEY_ID: ${TOS_ACCESS_KEY_ID:-}
|
|
TOS_SECRET_ACCESS_KEY: ${TOS_SECRET_ACCESS_KEY:-}
|
|
TOS_READ_URL_EXPIRES_SECONDS: ${TOS_READ_URL_EXPIRES_SECONDS:-43200}
|
|
TOS_FORCE_PATH_STYLE: ${TOS_FORCE_PATH_STYLE:-false}
|
|
ARK_API_KEY: ${ARK_API_KEY:-}
|
|
ARK_BASE_URL: ${ARK_BASE_URL:-https://ark.cn-beijing.volces.com/api/v3}
|
|
ARK_SEEDANCE_MODEL: ${ARK_SEEDANCE_MODEL:-doubao-seedance-2-0-260128}
|
|
ARK_SEEDANCE_FAST_MODEL: ${ARK_SEEDANCE_FAST_MODEL:-doubao-seedance-2-0-fast-260128}
|
|
ARK_SEEDANCE_MINI_MODEL: ${ARK_SEEDANCE_MINI_MODEL:-doubao-seedance-2-0-mini-260615}
|
|
ARK_SEEDANCE_25_MODEL: ${ARK_SEEDANCE_25_MODEL:-doubao-seedance-2-5-260628}
|
|
ARK_SEEDREAM_MODEL: ${ARK_SEEDREAM_MODEL:-doubao-seedream-5-0-pro-260628}
|
|
ARK_SEEDREAM_TIMEOUT_MS: ${ARK_SEEDREAM_TIMEOUT_MS:-180000}
|
|
ARK_POLL_INTERVAL_MS: ${ARK_POLL_INTERVAL_MS:-5000}
|
|
ARK_TASK_TIMEOUT_MS: ${ARK_TASK_TIMEOUT_MS:-900000}
|
|
ARK_DOWNLOAD_TIMEOUT_MS: ${ARK_DOWNLOAD_TIMEOUT_MS:-120000}
|
|
ARK_ARCHIVE_STALL_TIMEOUT_MS: ${ARK_ARCHIVE_STALL_TIMEOUT_MS:-30000}
|
|
ARK_ARCHIVE_TOTAL_TIMEOUT_MS: ${ARK_ARCHIVE_TOTAL_TIMEOUT_MS:-600000}
|
|
ARK_PROMPT_WIZARD_API_KEY: ${ARK_PROMPT_WIZARD_API_KEY:-}
|
|
ARK_PROMPT_WIZARD_MODEL: ${ARK_PROMPT_WIZARD_MODEL:-doubao-seed-2-0-lite-260428}
|
|
REAL_PORTRAIT_PROVIDER_MODE: ${REAL_PORTRAIT_PROVIDER_MODE:-disabled}
|
|
VOLC_ARK_ASSET_ACCESS_KEY_ID: ${VOLC_ARK_ASSET_ACCESS_KEY_ID:-}
|
|
VOLC_ARK_ASSET_SECRET_ACCESS_KEY: ${VOLC_ARK_ASSET_SECRET_ACCESS_KEY:-}
|
|
VOLC_ARK_ASSET_ENDPOINT: ${VOLC_ARK_ASSET_ENDPOINT:-https://ark.cn-beijing.volcengineapi.com/}
|
|
VOLC_ARK_ASSET_REGION: ${VOLC_ARK_ASSET_REGION:-cn-beijing}
|
|
VOLC_ARK_ASSET_SERVICE: ${VOLC_ARK_ASSET_SERVICE:-ark}
|
|
VOLC_ARK_ASSET_VERSION: ${VOLC_ARK_ASSET_VERSION:-2024-01-01}
|
|
VOLC_ARK_ASSET_PROJECT_NAME: ${VOLC_ARK_ASSET_PROJECT_NAME:-default}
|
|
REAL_PORTRAIT_PROVIDER_TIMEOUT_MS: ${REAL_PORTRAIT_PROVIDER_TIMEOUT_MS:-15000}
|
|
REAL_PORTRAIT_GROUP_SOFT_LIMIT: ${REAL_PORTRAIT_GROUP_SOFT_LIMIT:-45}
|
|
REAL_PORTRAIT_ASSET_SOFT_LIMIT: ${REAL_PORTRAIT_ASSET_SOFT_LIMIT:-45}
|
|
REAL_PORTRAIT_INVITE_TTL_SECONDS: ${REAL_PORTRAIT_INVITE_TTL_SECONDS:-1800}
|
|
REAL_PORTRAIT_TOKEN_ENCRYPTION_KEY: ${REAL_PORTRAIT_TOKEN_ENCRYPTION_KEY:-}
|
|
REAL_PORTRAIT_CONTROLLER_NAME: ${REAL_PORTRAIT_CONTROLLER_NAME:-}
|
|
REAL_PORTRAIT_PROCESSOR_NAME: ${REAL_PORTRAIT_PROCESSOR_NAME:-}
|
|
REAL_PORTRAIT_PRIVACY_CONTACT: ${REAL_PORTRAIT_PRIVACY_CONTACT:-}
|
|
REAL_PORTRAIT_PRIVACY_POLICY_URL: ${REAL_PORTRAIT_PRIVACY_POLICY_URL:-}
|
|
PROMPT_WIZARD_TIMEOUT_MS: ${PROMPT_WIZARD_TIMEOUT_MS:-120000}
|
|
PROMPT_WIZARD_REQUEST_BUDGET_MS: ${PROMPT_WIZARD_REQUEST_BUDGET_MS:-180000}
|
|
PROMPT_WIZARD_BATCH_BUDGET_MS: ${PROMPT_WIZARD_BATCH_BUDGET_MS:-240000}
|
|
PROMPT_WIZARD_MAX_RETRIES: ${PROMPT_WIZARD_MAX_RETRIES:-1}
|
|
PROMPT_WIZARD_RETRY_BASE_DELAY_MS: ${PROMPT_WIZARD_RETRY_BASE_DELAY_MS:-1000}
|
|
PROMPT_WIZARD_RETRY_MAX_DELAY_MS: ${PROMPT_WIZARD_RETRY_MAX_DELAY_MS:-8000}
|
|
PROMPT_WIZARD_QUEUE_CONCURRENCY: ${PROMPT_WIZARD_QUEUE_CONCURRENCY:-10}
|
|
PROMPT_WIZARD_PROVIDER_CONCURRENCY: ${PROMPT_WIZARD_PROVIDER_CONCURRENCY:-30}
|
|
VOLC_BILLING_ACCESS_KEY_ID: ${VOLC_BILLING_ACCESS_KEY_ID:-}
|
|
VOLC_BILLING_SECRET_ACCESS_KEY: ${VOLC_BILLING_SECRET_ACCESS_KEY:-}
|
|
VOLC_BILLING_ENDPOINT: ${VOLC_BILLING_ENDPOINT:-https://open.volcengineapi.com}
|
|
VOLC_BILLING_REGION: ${VOLC_BILLING_REGION:-cn-north-1}
|
|
VOLC_BILLING_SERVICE: ${VOLC_BILLING_SERVICE:-billing}
|
|
VOLC_BILLING_VERSION: ${VOLC_BILLING_VERSION:-2022-01-01}
|
|
VOLC_BILLING_SYNC_TTL_MS: ${VOLC_BILLING_SYNC_TTL_MS:-600000}
|
|
VOLC_BILLING_BALANCE_SYNC_INTERVAL_MS: ${VOLC_BILLING_BALANCE_SYNC_INTERVAL_MS:-300000}
|
|
VOLC_BILLING_BILL_SYNC_INTERVAL_MS: ${VOLC_BILLING_BILL_SYNC_INTERVAL_MS:-600000}
|
|
VOLC_BILLING_REQUEST_POLL_INTERVAL_MS: ${VOLC_BILLING_REQUEST_POLL_INTERVAL_MS:-5000}
|
|
VOLC_BILLING_REQUEST_TIMEOUT_MS: ${VOLC_BILLING_REQUEST_TIMEOUT_MS:-10000}
|
|
VOLC_BILLING_MAX_RETRIES: ${VOLC_BILLING_MAX_RETRIES:-4}
|
|
VOLC_BILLING_RETRY_BASE_DELAY_MS: ${VOLC_BILLING_RETRY_BASE_DELAY_MS:-500}
|
|
VOLC_BILLING_PAGE_DELAY_MS: ${VOLC_BILLING_PAGE_DELAY_MS:-250}
|
|
PAYMENT_CONFIG_ENCRYPTION_KEY: ${PAYMENT_CONFIG_ENCRYPTION_KEY:-}
|
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
|
RESEND_FROM: "${RESEND_FROM:-Lindyson AI <noreply@example.com>}"
|
|
RESEND_NOTIFY_TO: ${RESEND_NOTIFY_TO:-}
|
|
RESEND_BLOCKED_TO: ${RESEND_BLOCKED_TO:-}
|
|
QUEUE_CONCURRENCY: ${QUEUE_CONCURRENCY:-2}
|
|
MEDIA_COMMAND_TIMEOUT_MS: ${MEDIA_COMMAND_TIMEOUT_MS:-60000}
|
|
MEDIA_COMMAND_MAX_OUTPUT_BYTES: ${MEDIA_COMMAND_MAX_OUTPUT_BYTES:-33554432}
|
|
ENABLE_FAKE_PROVIDER: ${ENABLE_FAKE_PROVIDER:-false}
|
|
FAKE_PROVIDER_DELAY_MS: ${FAKE_PROVIDER_DELAY_MS:-1500}
|
|
|
|
|
|
services:
|
|
postgres:
|
|
restart: unless-stopped
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_DB: lindyson
|
|
POSTGRES_USER: lindyson
|
|
# 本地开发默认口令;对外部署时在 .env 里设置 POSTGRES_PASSWORD
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-lindyson}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U lindyson -d lindyson"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
|
|
redis:
|
|
restart: unless-stopped
|
|
image: redis:7-alpine
|
|
command: ["redis-server", "--appendonly", "yes"]
|
|
volumes:
|
|
- redis-data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
|
|
migrate:
|
|
build:
|
|
context: .
|
|
dockerfile: apps/api/Dockerfile
|
|
environment:
|
|
<<: *api-env
|
|
# 数据迁移不调用方舟,显式清空以免把生产凭据扩散到一次性容器。
|
|
ARK_API_KEY: ""
|
|
ARK_PROMPT_WIZARD_API_KEY: ""
|
|
REAL_PORTRAIT_PROVIDER_MODE: disabled
|
|
VOLC_ARK_ASSET_ACCESS_KEY_ID: ""
|
|
VOLC_ARK_ASSET_SECRET_ACCESS_KEY: ""
|
|
REAL_PORTRAIT_TOKEN_ENCRYPTION_KEY: ""
|
|
# 只有一次性的 migrate 容器拿到管理员初始化变量,长期运行的 api/worker 不拿。
|
|
BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL:-}
|
|
BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-}
|
|
BOOTSTRAP_RESET_ADMIN_PASSWORD: ${BOOTSTRAP_RESET_ADMIN_PASSWORD:-false}
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
command: ["npm", "--workspace", "apps/api", "run", "db:migrate:prod"]
|
|
|
|
|
|
api:
|
|
restart: unless-stopped
|
|
build:
|
|
context: .
|
|
dockerfile: apps/api/Dockerfile
|
|
environment: *api-env
|
|
depends_on:
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
ports:
|
|
# 只绑定本机回环地址;公网流量应经反向代理进入,避免直连伪造 X-Forwarded-For。
|
|
- "${API_HOST_BIND:-127.0.0.1}:${API_HOST_PORT:-4300}:4300"
|
|
volumes:
|
|
- local-storage:/app/storage
|
|
command: ["npm", "--workspace", "apps/api", "run", "start"]
|
|
|
|
|
|
worker:
|
|
restart: unless-stopped
|
|
build:
|
|
context: .
|
|
dockerfile: apps/api/Dockerfile
|
|
environment: *api-env
|
|
depends_on:
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
volumes:
|
|
- local-storage:/app/storage
|
|
command: ["npm", "--workspace", "apps/api", "run", "worker:start"]
|
|
|
|
|
|
web:
|
|
# 前端静态文件 + nginx 反代:/api、/mcp、/oauth 转给 api,其余返回网页。
|
|
image: ${WEB_IMAGE:-lindyson-web:local}
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.frontend
|
|
args:
|
|
APP_RELEASE_ID: ${APP_RELEASE_ID:-}
|
|
VITE_PUBLIC_SITE_URL: ${VITE_PUBLIC_SITE_URL:-}
|
|
VITE_SUPPORT_QR_URL: ${VITE_SUPPORT_QR_URL:-}
|
|
VITE_SEEDREAM_TUTORIAL_MEDIA_BASE_URL: ${VITE_SEEDREAM_TUTORIAL_MEDIA_BASE_URL:-}
|
|
depends_on:
|
|
- api
|
|
ports:
|
|
- "${WEB_HTTP_PORT:-80}:80"
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres-data:
|
|
redis-data:
|
|
local-storage:
|