Coral Docker Compose 部署指南

Coral 9.11.8 用 Docker Compose 从零部署的完整实测记录:镜像选型(talk:9.11.8 + mongo:8.0.3 + redis:7.2.5)、三服务编排、无头 API 安装与登录验证、数据持久化、环境变量速查、等价 docker run 与常用运维命令,全部命令与回显逐字取自 2026-11-02 实测。

CoralCoral评论系统DockerDocker ComposeMongoDBRedis2026/11/02

Coral Docker Compose 部署指南

目标:在 Ubuntu 22.04 服务器上用 Docker Compose 把 Coral 9.11.8(评论平台,Vox Media 开源)从空白目录部署到「已安装、可登录、可验证」的可用状态,覆盖镜像选型、三服务编排(Coral + MongoDB + Redis)、安装向导与无头 API 安装、数据持久化、环境变量、等价 docker run 与日常运维。本文全部命令与输出均逐字取自 2026-11-02 真实实测。产品定位与选型背景见 《Coral 介绍与选型指南》。

一、环境信息

本文实测基于以下环境(2026-11-02),全部版本号、端口、路径为真实取值:

  • 操作系统:Ubuntu 22.04 LTS(x86_64)
  • Docker Engine:29.9.2;Docker Compose:v5.6.1(docker --version / docker compose version 实测,见 3.1)
  • Coral:官方镜像 coralproject/talk:9.11.8(/api/version 实测返回 {"version":"9.11.8","revision":""})
  • 依赖服务:mongo:8.0.3、redis:7.2.5(与官方仓库 v9.11.8 标签下 docker-compose.yml 钉住的版本一致,见第二章)
  • 镜像内运行时:Node.js v18.20.8(docker inspect 实测 NODE_VERSION=18.20.8)
  • 项目目录:/opt/coral(存放 docker-compose.yml 与 .env)
  • 端口映射:127.0.0.1:8138 → 5000(Web / GraphQL API)、127.0.0.1:8139 → 9000(Prometheus 指标)
  • 数据目录:/opt/coral/data/mongo、/opt/coral/data/redis(bind 挂载)
  • 自定义网络:coral-net,显式子网 10.89.64.0/24
  • 演示管理员账号:root / q1w2e3r4(邮箱 root@example.com)

二、镜像选型说明

Coral 官方推荐的自托管形态就是 Docker:文档站安装页原文称 "all the dependencies are baked and shipped with the provided coralproject/talk image"。选型需要同时确定 Coral 镜像标签 与 两个依赖(MongoDB/Redis)的版本,官方三个来源的口径并不一致,实测对照如下(查询日期 2026-11-02):

来源CoralMongoDBRedis
文档站安装页「Docker 快速开始」示例coralproject/talk:7mongo:4.2redis:3.2
文档站安装页 Requirements—^8.0.3^3.2
GitHub 仓库 v9.11.8 标签的 docker-compose.yml—mongo:8.0.3redis:7.2.5

解读与取舍:安装页示例是历史遗留(talk:7/mongo:4.2 已明显落后),而 Requirements 明确 MongoDB 需 ^8.0.3,仓库当前编排文件实际钉住 mongo:8.0.3 + redis:7.2.5。本文按「当前版本线 + 仓库钉版」组合选型:coralproject/talk:9.11.8 + mongo:8.0.3 + redis:7.2.5——其中 9.11.8 为 GitHub Releases 实测的最新稳定版(发布于 2026-07-07)。

💡 技术拓展:容器镜像的版本锁定策略不止一种——

策略写法示例特点适用
latest 浮动coralproject/talk:latest永远最新,但重建即跳版,不可回滚试验环境
大版本通道coralproject/talk:9跟随 9.x 小版本滚动能接受小版本漂移
精确钉版coralproject/talk:9.11.8可复现、可回滚,升级需显式改标签生产(本文采用)

本文采用精确钉版:数据库类应用跨小版本可能携带 MongoDB 迁移,钉版 + 显式升级最稳妥。

镜像拉取实测(2026-11-02):

docker pull coralproject/talk:9.11.8
docker pull mongo:8.0.3
docker pull redis:7.2.5

三个镜像均分层 Pull complete 成功(本环境经代理拉取,速度较慢属正常)。

coralproject/talk:9.11.8 镜像内置默认配置实测(docker inspect 关键节,逐字):

["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NODE_VERSION=18.20.8","YARN_VERSION=1.22.22","NODE_OPTIONS=--max-old-space-size=8192 --openssl-legacy-provider --no-experimental-fetch","NODE_ENV=production","PORT=5000"] ["node","dist/index.js"] {"5000/tcp":{}} "node"

要点解读:镜像默认 NODE_ENV=production、PORT=5000(即容器内监听 5000,官方文档写的 PORT 默认 3000 只对源码运行生效);启动命令为 node dist/index.js;进程以非特权用户 node 运行;NODE_OPTIONS 内置 8GB 堆上限与 --openssl-legacy-provider(老算法签名兼容用)。

三、前置条件

3.1 确认 Docker 与 Compose 可用

Coral 官方部署形态依赖 Docker Compose,先确认宿主版本满足要求:

docker --version
docker compose version

实测回显:

Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1

Compose v2(docker compose 子命令)可用,本文命令统一用 v2 形态;旧版独立二进制 docker-compose 的等价命令把空格换成连字符即可。

3.2 端口空闲检查

本方案要占用宿主机 8138/8139 两个端口,先确认空闲:

ss -ltn | grep -E ':81(3[0-9]|4[0-9])'

实测结果(2026-11-02):该过滤仅命中 8133、8147、8148 三个端口(均被其他服务占用),8138–8142 全空闲,故本方案取 8138(Web)/8139(Metrics),后续实验预留 8140/8141。

3.3 网络子网检查

compose 显式指定子网,避免与既有自定义网络冲突:

docker network ls
docker network inspect $(docker network ls -q --filter 'driver=bridge') | grep -i subnet

实测 10.89 段仅 elgg-712-stack_elgg-net = 10.89.72.0/24 一个自定义子网被占用,槽 64 空闲,因此本编排显式钉 10.89.64.0/24。

3.4 生成 SIGNING_SECRET(必做)

SIGNING_SECRET 用于签发所有 JWT(会话、API 令牌、Webhook 签名关联),生产环境必须显式设置——沿用默认值 keyboard cat 时 NODE_ENV=production 下直接启动失败(见 7.2 实测)。官方推荐用 openssl 生成:

mkdir -p /opt/coral/data/mongo /opt/coral/data/redis
cd /opt/coral
openssl rand -base64 48 > /tmp/ss.b64
echo "SIGNING_SECRET=$(cat /tmp/ss.b64)" > .env && rm /tmp/ss.b64
grep -c '^SIGNING_SECRET=' .env

实测回显:

1

即 .env 中已写入一行 SIGNING_SECRET=<64 字符 base64 串>。该值是实例全部令牌体系的根密钥,属敏感信息,本文与任何文档、日志中均不回显其内容;丢失该值后所有已签发会话与 API 令牌全部失效,请纳入备份。

3.5 数据目录就绪

ls -la /opt/coral/data/

实测回显(首次创建后):

total 24
drwxr-xr-x 4 root    root  4096 Nov  2 11:21 .
drwxr-xr-x 3 root    root  4096 Nov  2 11:41 ..
drwxr-xr-x 4 dnsmasq root 12288 Nov  2 11:54 mongo
drwxr-xr-x 2 dnsmasq root  4096 Nov  2 11:53 redis

目录属主显示为镜像内运行用户的 UID 映射(mongo/redis 官方镜像以非 root 用户写数据),首次启动后由容器自行初始化,无需手工 chown。

四、部署步骤

4.1 编写编排文件

/opt/coral/docker-compose.yml 全文(实测文件逐字):

name: coral-9.11.8-stack

services:
  talk:
    image: coralproject/talk:9.11.8
    restart: always
    ports:
      - "127.0.0.1:8138:5000"
      - "127.0.0.1:8139:9000"
    depends_on:
      - mongo
      - redis
    environment:
      - MONGODB_URI=mongodb://mongo:27017/coral
      - REDIS_URI=redis://redis:6379
      - SIGNING_SECRET=${SIGNING_SECRET}
      - ENABLE_GRAPHIQL=true
      - METRICS_USERNAME=admin
      - METRICS_PASSWORD=q1w2e3r4
    networks:
      - coral-net

  mongo:
    image: mongo:8.0.3
    restart: always
    volumes:
      - ./data/mongo:/data/db
    networks:
      - coral-net

  redis:
    image: redis:7.2.5
    restart: always
    volumes:
      - ./data/redis:/data
    networks:
      - coral-net

networks:
  coral-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.64.0/24

逐项说明:

配置项取值说明
namecoral-9.11.8-stackCompose 项目名;实际生成的网络/容器名会把点号规范化为连字符(实测网络名 coral-9118-stack_coral-net)
talk.ports127.0.0.1:8138:5000仅绑回环,公网访问交由前置反代(Caddy/Nginx);5000 是镜像内置 PORT
MONGODB_URImongodb://mongo:27017/coral主机名 mongo 由 compose 网络内服务名解析
REDIS_URIredis://redis:6379同上,服务名 redis
SIGNING_SECRET${SIGNING_SECRET}从同目录 .env 注入(3.4 已生成),避免密钥进版本库
ENABLE_GRAPHIQLtrue开启 /graphiql 调试台,便于验证 API;生产建议关闭(见 7.3 与 FAQ)
METRICS_USERNAME/PASSWORDadmin / q1w2e3r4/metrics 的 Basic Auth(演示口令,生产请更换)
restart: always三个服务宿主机重启/Docker 守护重启后自动拉起
ipam.subnet10.89.64.0/24显式子网,防止与既有网络冲突(3.3 实测选槽)

4.2 启动编排

cd /opt/coral
docker compose up -d

实测回显(逐字):

 Network coral-9118-stack_coral-net Creating 
 Network coral-9118-stack_coral-net Creating 
 Network coral-9118-stack_coral-net Created 
 Network coral-9118-stack_coral-net Created 
 Container coral-9118-stack-redis-1 Creating 
 Container coral-9118-stack-mongo-1 Creating 
 Container coral-9118-stack-redis-1 Created 
 Container coral-9118-stack-mongo-1 Created 
 Container coral-9118-stack-talk-1 Creating 
 Container coral-9118-stack-talk-1 Created 
 Container coral-9118-stack-redis-1 Starting 
 Container coral-9118-stack-mongo-1 Starting 
 Container coral-9118-stack-redis-1 Started 
 Container coral-9118-stack-mongo-1 Started 
 Container coral-9118-stack-talk-1 Starting 
 Container coral-9118-stack-talk-1 Started

4.3 验证三服务在跑

docker compose ps

实测回显(逐字):

NAME                       IMAGE                      COMMAND                  SERVICE   CREATED        STATUS                  PORTS
coral-9118-stack-mongo-1   mongo:8.0.3                "docker-entrypoint.s…"   mongo     1 second ago   Up Less than a second   27017/tcp
coral-9118-stack-redis-1   redis:7.2.5                "docker-entrypoint.s…"   redis     1 second ago   Up Less than a second   6379/tcp
coral-9118-stack-talk-1    coralproject/talk:9.11.8   "docker-entrypoint.s…"   talk      1 second ago   Up Less than a second   127.0.0.1:8138->5000/tcp, 127.0.0.1:8139->9000/tcp

三个服务全部 Up,talk 的两个端口映射正确。

4.4 查看启动日志确认就绪

docker compose logs talk

实测回显(输出节选,实测首尾):

talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"msg":"mongodb has connected","time":"2026-11-02T03:21:52.544Z","src":{"file":"/usr/src/app/server/src/core/server/services/mongodb/index.ts","line":58,"func":"createMongoDB"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"queries":236,"msg":"loaded persisted queries","time":"2026-11-02T03:21:52.589Z","src":{"file":"/usr/src/app/server/src/core/server/graph/persisted/loader.ts","line":64,"func":"loadPersistedQueries"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"msg":"no tenants are installed, skipping running migrations","time":"2026-11-02T03:21:52.614Z","src":{"file":"/usr/src/app/server/src/core/server/index.ts","line":379,"func":"process"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"msg":"job processing is enabled, starting job processors","time":"2026-11-02T03:21:54.437Z","src":{"file":"/usr/src/app/server/src/core/server/index.ts","line":390,"func":"process"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"msg":"adding authentication to metrics endpoint","time":"2026-11-02T03:21:54.446Z","src":{"file":"/usr/src/app/server/src/core/server/app/index.ts","line":290,"func":"createMetricsServer"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"port":9000,"path":"/metrics","msg":"now listening for metrics","time":"2026-11-02T03:21:54.449Z","src":{"file":"/usr/src/app/server/src/core/server/index.ts","line":420,"func":"process"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"env":"production","forceSSL":false,"level":40,"msg":"FORCE_SSL=true should be set when a SSL terminating proxy has been configured","time":"2026-11-02T03:21:54.450Z","src":{"file":"/usr/src/app/server/src/core/server/app/index.ts","line":246,"func":"configureApplicationHTTPS"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":40,"msg":"it is not recommended to have enable_graphiql enabled while in production mode as it requires introspection queries to be enabled","time":"2026-11-02T03:21:54.518Z","src":{"file":"/usr/src/app/server/src/core/server/app/router/index.ts","line":78,"func":"attachGraphiQL"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"port":5000,"msg":"now listening","time":"2026-11-02T03:21:54.588Z","src":{"file":"/usr/src/app/server/src/core/server/index.ts","line":482,"func":"start"},"v":0}

关键行解读:mongodb has connected(Mongo 连通)→ loaded persisted queries(加载 236 条持久化查询白名单)→ no tenants are installed(空库,等待安装)→ now listening for metrics(9000)→ now listening(5000)。两条 level:40 警告分别对应 FORCE_SSL 与 ENABLE_GRAPHIQL,属预期提示(见第七章)。日志中还出现 MaxListenersExceededWarning 与 failed to set logging project id(Google Cloud 探测失败)两条噪音,均为已知无害输出(见 11.8)。

4.5 验证 HTTP 可用性

curl -s -w '\n/api/version -> %{http_code}\n' http://127.0.0.1:8138/api/version
curl -s -w '\n/api/health -> %{http_code}\n' http://127.0.0.1:8138/api/health
curl -s -o /dev/null -w '/ -> %{http_code} redirect=%{redirect_url}\n' http://127.0.0.1:8138/

实测回显(逐字):

{"version":"9.11.8","revision":""}
/api/version -> 200
OK
/api/health -> 200
/ -> 302 redirect=http://127.0.0.1:8138/install

/api/version 报告 9.11.8;健康检查 OK;根路径 302 跳 /install——说明服务正常但尚未安装租户,下一步进入安装。

4.6 安装方式一:浏览器安装向导

浏览器访问 http://127.0.0.1:8138,被 302 带到 /install 向导,依次填写:

  1. Organization:媒体组织名、URL、联系邮箱;
  2. Site:站点名(本文用 demo-site)与允许嵌入评论流的域名(Permitted Domains,本文用 http://127.0.0.1:8138);
  3. 管理员账号:用户名 root、邮箱 root@example.com、密码 q1w2e3r4。

向导提交后自动执行 MongoDB 迁移并写入首个租户,随后重定向进 /admin 管理控制台。

4.7 安装方式二:无头 API 安装(无人值守场景)

安装向导本质是调 POST /api/install,服务器无浏览器/需自动化时可直接调用。先确认未安装状态:

curl -s -o /dev/null -w 'GET /api/install -> %{http_code}\n' http://127.0.0.1:8138/api/install

实测回显:

GET /api/install -> 204

204 No Content 表示「可安装」。发起安装(body 结构即向导三步的数据,租户域名自动取请求 Host):

curl -s -o /dev/null -w 'POST /api/install -> %{http_code}\n' \
  -X POST http://127.0.0.1:8138/api/install \
  -H 'Content-Type: application/json' \
  -H 'Host: 127.0.0.1:8138' \
  -d '{
    "tenant": {"organization": {"name": "Demo Media", "url": "http://127.0.0.1:8138", "contactEmail": "root@example.com"}},
    "site":   {"name": "demo-site", "allowedOrigins": ["http://127.0.0.1:8138"]},
    "user":   {"username": "root", "password": "q1w2e3r4", "email": "root@example.com"}
  }'

实测回显:

POST /api/install -> 204

204 即安装成功。服务端日志同步记录了迁移与租户落库过程(输出节选,实测首尾):

talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"pending":4,"msg":"executing pending migrations","time":"2026-11-02T03:24:52.223Z","src":{"file":"/usr/src/app/server/src/core/server/services/migrate/manager.ts","line":177,"func":"executePendingMigrations"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"migrationName":"indexes","migrationID":1579189174930,"level":30,"executionTime":209,"msg":"finished index migration","time":"2026-11-02T03:24:52.440Z","src":{"file":"/usr/src/app/server/src/core/server/services/migrate/manager.ts","line":201,"func":"executePendingMigrations"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"migrationName":"create_sites","migrationID":1579189174931,"level":30,"executionTime":9,"msg":"finished index migration","time":"2026-11-02T03:24:52.452Z","src":{"file":"/usr/src/app/server/src/core/server/services/migrate/manager.ts","line":201,"func":"executePendingMigrations"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"finishTime":230,"currentMigrationID":1582929716101,"msg":"finished running pending migrations","time":"2026-11-02T03:24:52.455Z","src":{"file":"/usr/src/app/server/src/core/server/services/migrate/manager.ts","line":258,"func":"executePendingMigrations"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"msg":"installing tenant","time":"2026-11-02T03:24:52.455Z","src":{"file":"/usr/src/app/server/src/core/server/services/tenant/tenant.ts","line":194,"func":"install"},"v":0}
talk-1  | {"name":"coral","hostname":"13a177526b8d","pid":1,"level":30,"tenantID":"2d8dd6dc-bc2f-4973-bca4-c246f9051239","msg":"a tenant has been installed","time":"2026-11-02T03:24:52.459Z","src":{"file":"/usr/src/app/server/src/core/server/services/tenant/tenant.ts","line":202,"func":"install"},"v":0}

4 个待执行迁移(含 indexes、create_sites)在 230ms 内完成,租户 ID 2d8dd6dc-… 落库。

⚠️ 注意:/api/install 有按 IP 的限流(check 接口 10 秒 2 次、install 接口 10 秒 1 次,源码实测),脚本重试时不要高频打;已安装过的实例再调 install 会返回 TenantInstalledAlreadyError,重复安装多租户需 management 令牌(源码 checkForInstallationToken 逻辑),常规部署一个实例一个向导即可。

4.8 安装后验证

curl -s -o /dev/null -w '/ -> %{http_code} redirect=%{redirect_url}\n' http://127.0.0.1:8138/
curl -s -o /dev/null -w '/install -> %{http_code} redirect=%{redirect_url}\n' http://127.0.0.1:8138/install
curl -s -o /dev/null -w '/admin -> %{http_code}\n' http://127.0.0.1:8138/admin

实测回显(逐字):

/ -> 302 redirect=http://127.0.0.1:8138/admin
/install -> 302 redirect=http://127.0.0.1:8138/admin
/admin -> 200

根路径不再跳 /install 而跳 /admin,管理控制台 200——安装闭环。再用管理员凭据换取会话令牌并查询 GraphQL 验证账号与站点:

curl -s -X POST http://127.0.0.1:8138/api/auth/local \
  -H 'Content-Type: application/json' \
  -d '{"usernameOrEmail":"root","password":"q1w2e3r4"}'

实测回显(令牌节选,完整值含签名段):

HTTP 200
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3OTM1OTAwMjcsImV4cCI6MTgwMTM2NjAyNywiaXNzIjoiMmQ4ZGQ2ZGMt…"}

用该令牌调 GraphQL 查 viewer 与 sites:

curl -s -X POST http://127.0.0.1:8138/api/graphql \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"query":"{ viewer { id username email role } sites { nodes { id name allowedOrigins createdAt } } }"}'

实测回显(逐字):

{"data":{"viewer":{"id":"9d72f9a7-8e8e-4ce8-b0dc-7311c5df4ddf","username":"root","email":"root@example.com","role":"ADMIN"},"sites":{"nodes":[{"id":"ceb9bf0f-01c1-42f8-b91c-a3f8da7876d0","name":"demo-site","allowedOrigins":["http://127.0.0.1:8138"],"createdAt":"2026-11-02T03:24:52.459Z"}]}}}

viewer.role = ADMIN、站点 demo-site 的 createdAt 与 4.7 的租户安装日志时间完全一致(03:24:52.459Z),账号、站点、租户三者对上了。

4.9 验证评论流嵌入资产

Coral 给宿主文章页用的嵌入脚本由服务端直接提供:

curl -s -o /dev/null -w '/assets/js/embed.js -> %{http_code} %{size_download}B\n' http://127.0.0.1:8138/assets/js/embed.js

实测回显:

/assets/js/embed.js -> 200 108227B

108KB 的 embed.js 正常下发,把它按官方片段插入任意文章页即可拉起评论流(集成细节见 《Coral 9.11.8 API 使用详解》)。

五、数据持久化说明

两个依赖服务的数据全部落在项目目录的 bind mount 上:

挂载容器内路径内容
./data/mongo:/data/dbMongoDB 数据目录租户、站点、故事、评论、用户、审核动作等全部业务数据
./data/redis:/dataRedis 持久化目录pub/sub 广播与任务队列的 RDB/AOF 快照(可从 Mongo 重建,重要性低)

实测运行一段时间后的目录体量(du -sh data/*,逐字):

203M	data/mongo
2.0M	data/redis

MongoDB 初始库即 203MB 主要是索引与集合骨架,属正常水位。

重启不丢数据的实测验证:重建 talk 服务后回查安装期写入的故事与评论:

docker compose restart talk

实测回显(逐字):

 Container coral-9118-stack-talk-1 Restarting 
 Container coral-9118-stack-talk-1 Started
curl -s -X POST http://127.0.0.1:8138/api/graphql \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"query":"{ story(id: \"14cb4c15-54bc-42fe-a7f7-e63bff2b0f14\") { commentCounts { totalPublished } } }"}'

实测回显(逐字):

{"data":{"story":{"url":"http://127.0.0.1:8138/news/demo-article-1","commentCounts":{"totalPublished":1}}}}

服务重建后评论计数仍为 1,数据在盘不在容器。docker compose down(删容器不删卷)再 up -d 后 /api/version 与 / → 302 /admin 均与重建前一致,同样验证了持久化(回显见第十章)。

⚠️ 注意:docker compose down -v 会连卷一起删;本方案用的是 bind mount(./data 目录),down -v 不会删目录本身,但误删目录即丢全部评论数据,备份策略见第十章。

六、环境变量表

Coral 实例级配置全部走环境变量(官方 Environment Variables 页全文口径,默认值逐项核对自 tmp 存档原文)。必需 4 项:

变量默认值说明
PORT文档 3000 / 镜像内 5000HTTP 与 WebSocket 监听端口(镜像 ENV 实测 5000,见 7.1)
MONGODB_URImongodb://127.0.0.1:27017/coralMongoDB 连接串
REDIS_URIredis://127.0.0.1:6379Redis 连接串
SIGNING_SECRETkeyboard catJWT 签名密钥;production 下沿用默认值直接启动失败(实测见 7.2)

高级配置:

变量默认值说明
NODE_ENVproduction(Docker/start)production 或 development,生产必须 production
REDIS_OPTIONS{}连接 Redis 的附加选项(JSON 串,ioredis 参数)
SIGNING_ALGORITHMHS256JWT 签名算法,支持 HS256/384/512、RS256/384/512、ES256/384/512
LOCALEen-US默认语言
LOGGING_LEVELinfofatal/error/warn/info/debug/trace
FORCE_SSLfalse强制 HTTPS 跳转并发送 HSTS(实测见 7.4)
DISABLE_LIVE_UPDATESfalse全局关闭评论流 WebSocket
DISABLE_LIVE_UPDATES_TIMEOUT2 weeks故事空闲超时后暂停实时推送(ms 格式)
DISABLE_TENANT_CACHINGfalse关闭租户内存缓存,每次查库
ENABLE_GRAPHIQLfalse开启 /graphiql 调试台并放宽持久化查询限制(实测见 7.3)
METRICS_USERNAME / METRICS_PASSWORD未设则 /metrics 无认证指标端点 Basic Auth 账号
METRICS_PORT9000Prometheus 指标端口
SCRAPE_TIMEOUT10 seconds故事抓取超时(ms 格式)
SCRAPE_MAX_RESPONSE_SIZE10e6抓取响应大小上限(字节)
STATIC_URI未设静态资源独立分发地址(CDN/反代)
TRUST_PROXY未设Express "trust proxy" 设置,反代后修 URL 协议/重定向环
WEBSOCKET_KEEP_ALIVE_TIMEOUT30sWebSocket 保活间隔
WORD_LIST_TIMEOUT100ms词表匹配请求超时
PERSPECTIVE_TIMEOUT800msPerspective API 请求超时

仅开发模式生效:

变量默认值说明
DEV_PORT8080Webpack 开发服务器端口
DISABLE_RATE_LIMITERSfalse关闭内置限流(生产禁用)

解读:本文编排实际设置了 6 项——4 个必需项中的 3 个(PORT 用镜像默认 5000)加 ENABLE_GRAPHIQL(调试期开、生产关)与 METRICS_*(给 /metrics 上锁)。其余全部保持默认即可跑通;每个参数在生产环境的取值建议与逐项实测,见 《Coral 9.11.8 配置参数详解》。

七、配置参数说明(运行时实测佐证)

本章对第六章中最容易踩坑的 5 组参数给出部署环境的实测行为,全部回显为逐字记录。

7.1 PORT:文档默认 3000,镜像默认 5000

docker inspect 实测镜像 ENV(逐字,见第二章)含 "PORT=5000",启动日志 {"port":5000,"msg":"now listening"} 印证。结论:用官方镜像时容器内端口恒为 5000,对外端口靠映射改,不要改 PORT 造成反代上游错位。

7.2 SIGNING_SECRET 生产护栏(实测启动失败案例)

用默认值 keyboard cat 且 NODE_ENV=production 启动,服务直接拒绝 bootstrap:

docker run --rm -e NODE_ENV=production -e SIGNING_SECRET=keyboard cat \
  -e MONGODB_URI=mongodb://mongo:27017/coral -e REDIS_URI=redis://redis:6379 \
  --network coral-9118-stack_coral-net coralproject/talk:9.11.8

实测回显(服务日志,逐字):

{"name":"coral","hostname":"731abaca4620","pid":1,"level":50,"err":{"message":"SIGNING_SECRET is required in production environments","name":"Error","stack":"new Server (src/core/server/index.ts:156:15)\ncreateCoral (src/core/index.ts:9:10)\nbootstrap (src/index.ts:59:31)\nObject.<anonymous> (src/index.ts:75:1)\n"},"msg":"can not bootstrap server","time":"2026-11-02T03:36:43.294Z","src":{"file":"/usr/src/app/server/src/index.ts","line":70,"func":"bootstrap"},"v":0}

这是官方文档预告的刻意行为("Coral will throw a runtime error … to prevent insecure installations")。部署时务必按 3.4 注入真实密钥。

7.3 ENABLE_GRAPHIQL:调试与安全的开关

  • 开启(本编排)时,production 模式会打警告(4.4 日志倒数第 2 条 level:40),因为它同时放宽了「只允许持久化查询」的安全限制;
  • 关闭后实测(第二实例 ENABLE_GRAPHIQL=false):/graphiql -> 404,且未走持久化白名单的原始 GraphQL 查询被拒(逐字):
{"error":{"id":"b57dc060-d66e-11f1-adad-d3d6510464fa","traceID":"b57d9950-d66e-11f1-adad-d3d6510464fa","code":"RAW_QUERY_NOT_AUTHORIZED","type":"INVALID_REQUEST_ERROR","message":"You are not authorized to execute this query."},"report":null}

而开启的实例上同一原始查询直接成功({"data":{"sites":{"nodes":[{"name":"demo-site"}]}}})。生产部署建议删除编排里的 ENABLE_GRAPHIQL=true,API 集成改用持久化查询或按 D5 篇的令牌方式调用。

7.4 FORCE_SSL:301 + HSTS(实测 180 天)

FORCE_SSL=true 的第二实例实测(逐字):

GET /admin -> 301 redirect=https://127.0.0.1:8141/admin
HTTP/1.1 301 Moved Permanently
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=15552000
Location: https://127.0.0.1:8141/admin
Vary: Accept
Content-Type: text/plain; charset=utf-8
Content-Length: 62
Date: Mon, 02 Nov 2026 03:38:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

要点:① 只做 301 跳转与 HSTS 头,不提供任何证书——https:// 直连本实例返回 HTTP/1.1 403 Forbidden(实测),TLS 必须由前置反代终结;② HSTS max-age=15552000 秒 = 180 天,官方文档写的是 60 天,本文以实测为准——开启前要确保 HTTPS 至少能稳定供给半年,否则浏览器会拒绝降级;③ 反代后出现重定向环时配 TRUST_PROXY(官方排障口径)。

7.5 METRICS_*:指标端点加锁

未配账号时 /metrics 裸奔;本编排配了 admin/q1w2e3r4,实测:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8139/metrics
curl -s -u admin:q1w2e3r4 http://127.0.0.1:8139/metrics | head -6

实测回显(输出节选,实测首尾):

401
# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter
process_cpu_user_seconds_total 3.60966

# HELP coral_executed_graph_queries_total number of GraphQL queries executed
# HELP coral_executed_graph_queries_timings timings for execution times of GraphQL operations
# HELP coral_connected_websockets_total number of websocket connections being handled
coral_executed_graph_queries_total{operation_type="mutation",operation_name="CreateTokenMutation"} 2
coral_executed_graph_queries_total{operation_type="query",operation_name="ListTokenQuery"} 2
coral_executed_graph_queries_total{operation_type="mutation",operation_name="DeactivateTokenMutation"} 1

无凭证 401,Basic Auth 通过后输出标准 Prometheus 文本,Coral 专属指标 3 个:coral_executed_graph_queries_total(按操作名计数)、coral_executed_graph_queries_timings(执行耗时直方图)、coral_connected_websockets_total(WS 连接数)。

八、配置文件挂载

Coral 没有配置文件:实例级配置只有环境变量(官方文档原文:可 export NODE_ENV=development 或在源码 server/ 目录下放 .env 文件,格式 KEY=VALUE 换行分隔;Docker 部署走 compose environment)。本方案的落盘文件与挂载清单:

文件/挂载路径作用变更方式
编排文件/opt/coral/docker-compose.yml服务拓扑与环境变量编辑后 docker compose up -d(仅重建受影响服务)
密钥文件/opt/coral/.env提供 ${SIGNING_SECRET} 插值编辑后 docker compose up -d talk(重建生效,无热加载)
Mongo 数据卷./data/mongo:/data/db全部业务数据不手工改动
Redis 数据卷./data/redis:/datapub/sub 与队列快照不手工改动

💡 技术拓展:Docker 场景注入配置的常见形态对比——

形态载体特点
compose environment 内联编排文件直观,但敏感值会进版本库
.env 插值 ${VAR}项目目录 .env密钥与编排分离(本文采用)
env_file: 整文件注入独立 env 文件变量多时省事,排查优先级较绕
secrets/挂载文件文件挂载适合 TLS 证书类大对象,Coral 不支持从文件读配置

Coral 只认环境变量,故本文用「.env + ${SIGNING_SECRET} 插值」把唯一的密钥项与编排分离。

租户级配置(站点、审核策略、SSO、Webhook 等)不在环境里,存 MongoDB、经 /admin 管理台或 GraphQL API 修改,改完即时生效、无需重启——这条边界在 C1 篇有完整展开。

九、等价 docker run

不用 Compose、手工三条 docker run 可复现同一拓扑(依赖与 talk 分开起):

docker network create --subnet 10.89.64.0/24 coral-net
docker run -d --name mongo --network coral-net \
  -v /opt/coral/data/mongo:/data/db mongo:8.0.3
docker run -d --name redis --network coral-net \
  -v /opt/coral/data/redis:/data redis:7.2.5
docker run -d --name coral-run-check --network coral-net \
  -p 127.0.0.1:8140:5000 \
  -e MONGODB_URI=mongodb://mongo:27017/coral \
  -e REDIS_URI=redis://redis:6379 \
  -e SIGNING_SECRET="$(grep '^SIGNING_SECRET' /opt/coral/.env | cut -d= -f2-)" \
  coralproject/talk:9.11.8

talk 一条实测回显(逐字;容器 ID 为本次实测值,你环境会不同):

dd7ef3ce568ce42b1245df56fc83702e385dc837421c7caf89ec2dc5ebab2c67
200
{"version":"9.11.8","revision":""}
/ -> 302 redirect=http://127.0.0.1:8140/admin

验证要点:该实例复用同一 Mongo 数据卷,起来后 /api/version 返回 200,且根路径直接 302 到 /admin(而非 /install)——证明它读到了已安装的租户,与 compose 版行为完全等价。验证完清理:

docker rm -f coral-run-check

实测回显:

coral-run-check

⚠️ 注意:手工 run 没有 depends_on 与 restart: always 的编排语义,宿主机重启后需自行拉起,生产建议仍用 Compose(或 systemd 托管 docker service)。

十、常用运维命令

目的命令说明
状态总览docker compose ps回显见 4.3
跟踪日志docker compose logs -f talkJSON 结构化日志,level 50=error/40=warn/30=info
重启应用docker compose restart talk不动数据,回显见第五章
改配置生效编辑 compose/.env → docker compose up -d仅重建配置有变化的服务
停止整栈docker compose stop保留容器
下线整栈docker compose down删容器和网络,保留 ./data 数据卷
重新上线docker compose up -d数据原样恢复

down / 重新 up -d 实测回显(逐字,输出节选):

 Container coral-9118-stack-talk-1 Stopping 
 Container coral-9118-stack-talk-1 Stopped 
 Container coral-9118-stack-talk-1 Removing 
 Container coral-9118-stack-talk-1 Removed 
 Container coral-9118-stack-redis-1 Stopping 
 Container coral-9118-stack-mongo-1 Stopping 
 Container coral-9118-stack-mongo-1 Stopped 
 Container coral-9118-stack-mongo-1 Removing 
 Container coral-9118-stack-redis-1 Stopped 
 Container coral-9118-stack-redis-1 Removed 
 Network coral-9118-stack_coral-net Removing 
 Network coral-9118-stack_coral-net Removed
 Network coral-9118-stack_coral-net Creating 
 Network coral-9118-stack_coral-net Created 
 Container coral-9118-stack-redis-1 Created 
 Container coral-9118-stack-mongo-1 Created 
 Container coral-9118-stack-talk-1 Created 
 Container coral-9118-stack-redis-1 Started 
 Container coral-9118-stack-mongo-1 Started 
 Container coral-9118-stack-talk-1 Started

重建后验证(逐字):

{"version":"9.11.8","revision":""}

/api/version 依旧 200,租户/站点/故事数据全部原样(持久化机制见第五章)。

升级与回滚:把 compose 中 image: coralproject/talk:9.11.8 改为目标版本 → docker pull → docker compose up -d;新版本启动时自动执行待处理迁移(4.7 日志即迁移实录)。回滚 = 标签改回旧版重新 up -d,但已执行的新版迁移不保证向后兼容,回滚前先备份 data/mongo。

备份(文件级,停机窗口最小):

docker compose stop talk
tar -czf coral-mongo-$(date +%F).tar.gz -C /opt/coral/data mongo
docker compose start talk

十一、常见问题

11.1 /api/v1 返回 500

实测(逐字):

/api/v1 -> 500
{"version":"9.11.8","revision":""}
/api/version -> 200

官方 Troubleshooting 页写明:/api/v1 只对 4.x 版本有效,5.x 起版本端点是 /api/version。9.x 上 /api/v1 返回 500 属预期,监控探活请用 /api/version 或 /api/health。

11.2 启动即退出:SIGNING_SECRET is required in production environments

现象与逐字日志见 7.2。原因是 SIGNING_SECRET 未设置或沿用默认值 keyboard cat。解决:按 3.4 生成真实密钥写入 .env,docker compose up -d talk 重建。

11.3 端口被占:映射冲突

8138/8139 若被他服务占用(3.2 的检查方法),改 compose 中冒号左侧的宿主端口即可(如 "127.0.0.1:9080:5000"),容器内 5000 不动;同时注意安装时 site.allowedOrigins 与租户域名要和新访问地址一致,否则嵌入评论流会被 Origin 校验拒绝。

11.4 子网冲突:Pool overlaps with other one on this address space

显式 10.89.64.0/24 若与既有 Docker 网络重叠,按 3.3 的方法找出占用段,换一个空闲槽再 docker compose up -d。

11.5 Webhook 端点用 http 被拒(production)

production 实例创建 http:// 的 webhook endpoint 实测被拒(逐字):

{"errors":[{"message":"Internal Error","locations":[{"line":1,"column":12}],"path":["createWebhookEndpoint"],"extensions":{"code":"INTERNAL_ERROR","id":"f75167d0-d66e-11f1-8984-e9eacc17974c","traceID":"f74f9310-d66e-11f1-8984-e9eacc17974c","type":"INVALID_REQUEST_ERROR","message":"Internal Error"}}],"data":null}

服务端日志给出真实原因(逐字):

"originalError":{"message":"invalid scheme provided in production: http:","name":"Error","stack":"validateWebhookEndpointInput (src/core/server/services/tenant/webhookEndpoints.ts:35:11)\ncreateWebhookEndpoint (src/core/server/services/tenant/webhookEndpoints.ts:54:3)\n

即 production 模式强制 webhook 接收端为 https。内网测试接收端要么上 TLS,要么在 NODE_ENV=development 实例验证(实测 dev 实例允许 http 且推送成功,见 《Coral 9.11.8 API 使用详解》)。GraphQL 层把该错误包装成 INTERNAL_ERROR,排障时看服务日志里的 originalError 才见真相。

11.6 开了 FORCE_SSL 后浏览器进不去

HSTS 实测 180 天(7.4),一旦访问过,浏览器强制 https;若你的反代没配好证书,站点表现为彻底不可达。缓解:测试环境不要开 FORCE_SSL;已中招需清浏览器 HSTS 状态(Chrome: chrome://net-internals/#hsts)。反代后管理页 URL 显示 http 或重定向环,配 TRUST_PROXY(官方口径)。

11.7 重复调 POST /api/install 报 TenantInstalledAlreadyError

安装接口按 IP 限流(install 10 秒 1 次)且一域名只可装一次;已装实例再调返回 TenantInstalledAlreadyError。同实例加第二租户属 management API 场景(需 MANAGEMENT_SIGNING_SECRET 与安装令牌),常规单站点部署用不到。

11.8 日志里的两条「报错」噪音

4.4 日志中的 (node:1) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 12 error listeners added to [Redis] 与 failed to set logging project id Error: Unable to detect a Project Id 两条,实测在正常运行的实例上持续存在:前者是 Redis 多监听器的 Node 告警(无泄漏实害),后者是 Google Cloud 日志项目探测失败(未接 GCP 属预期)。均可忽略,不影响服务。

十二、参考链接

Coral Docker Compose 部署指南 - Quzuo