Novu Docker 自托管与本地开发指南:从 Compose 依赖栈到生产级部署
本指南以仓库 docker/Readme.md 为核心,完整讲解 Novu 项目在 Docker 生态下的两种用法:用 docker/local 的 Compose 文件拉起本地开发所需的 MongoDB、Redis、LocalStack 等依赖服务,然后从源码运行 Novu 应用本身;以及参考 docker/community 下的镜像编排栈做面向生产的自托管部署。读完本文,你将能独立完成本地开发环境搭建、理解各 Compose 文件差异与端口分配、正确配置 JWT / 加密等核心密钥,并掌握 Docker 自托管时的环境变量体系与安全加固要点。
docker/ 目录到底放了什么
仓库根目录的 docker/ 下只有两个子目录,职责划分非常清晰:
- docker/local/:面向本地开发的 Compose 编排,含
docker-compose.yml、docker-compose.agent.yml、docker-compose.e2e.yml、docker-compose.local.yml四个文件; - docker/community/:面向自托管部署的
docker-compose.yml、.env.example以及自动化安装脚本 setup.sh。
原文档特别强调了一句话,也是理解整套编排的钥匙:
The
docker/local/compose file is for local development dependencies only — it does not start the Novu API, Worker, WebSocket, or Dashboard services.
即 docker/local/docker-compose.yml 只启动开发依赖(数据库、缓存、对象存储模拟器),并不会启动 Novu 自己的任何应用进程。API、Worker、WebSocket、Dashboard 这些服务需要通过 pnpm dev:portless 从源码直接跑起来(详见下文),这是本地开发模式与自托管镜像模式在架构上的根本区别。
docker/local:一份"只含依赖"的 Compose 清单
启动全部依赖服务
从仓库根目录执行:
docker compose -f docker/local/docker-compose.yml up -d
启动后你会得到四个容器。以仓库实际内容为准,docker-compose.yml 中定义的依赖及其端口如下:
| 服务 | 镜像 | 容器名(默认) | 端口(默认) | 用途 |
|---|---|---|---|---|
| localstack | localstack/localstack:0.14.5 |
${LOCALSTACK_DOCKER_NAME:-localstack_main} |
4566(${DOCKER_LOCALSTACK_PORT:-4566}) |
以 SERVICES=s3 模式提供 S3 对象存储模拟,用于上传工作流附件、邮件图片等 |
| mongo | mongo:8.0.17 |
${MONGO_DOCKER_NAME:-mongo_main} |
27017(${DOCKER_MONGO_PORT:-27017}) |
主数据库,存储组织、工作流、订阅者、消息等业务数据 |
| redis | redis |
${REDIS_DOCKER_NAME:-redis_main} |
6379(${DOCKER_REDIS_SERVICE_PORT:-6379}) |
队列与缓存,支撑 Worker 的作业分发与缓存服务 |
| clickhouse | clickhouse/clickhouse-server:24.3-alpine |
${CLICKHOUSE_DOCKER_NAME:-clickhouse_main} |
8123(HTTP)、9000(Native) |
消息活动(activity)等分析型数据存储,端口对应变量 DOCKER_CLICKHOUSE_HTTP_PORT、DOCKER_CLICKHOUSE_NATIVE_PORT |
值得注意的编排细节:
- 全部采用
network_mode: bridge,容器之间不共享自定义网络,而是把端口直接暴露给宿主机,方便宿主机上的 Node 进程(即从源码运行的 Novu 服务)通过localhost直连。 - 数据卷默认挂到本机临时目录:Mongo 与 LocalStack 使用
${TMPDIR:-/tmp/...},意味着默认情况下数据不持久化到命名卷,重启或清理后数据会丢失,只适合短期开发。 - 镜像与端口都可被环境变量覆盖,Compose 语法形如
'${DOCKER_MONGO_PORT:-27017}:27017',即在未设置该环境变量时回退到默认端口。若你本机27017已被占用,可通过设置DOCKER_MONGO_PORT等变量避开冲突。 - 健康检查:mongo 通过
mongo --eval "printjson(rs.status())"、redis 通过redis-cli ping、localstack 通过aws s3 ls探测,均设置retries: 5、interval: 10s,为后续其它服务的depends_on.condition: service_healthy预留前提。 - ClickHouse 声明了资源配额:
deploy.resources.limits.memory: 2G、reservations.memory: 1G,提示该服务是四个依赖中内存占用最大的一个。
只启动需要的服务(规避端口冲突)
如果你的机器上已有 MongoDB 或 Redis 在运行,没必要启动全套:
docker compose -f docker/local/docker-compose.yml up -d redis
同理,mongo、localstack、clickhouse 都可以作为单独的参数传入,docker compose ... up -d <service> 只会拉起你指定的服务。
同一目录下的其它三个 Compose 文件
除了默认的 docker-compose.yml,docker/local 下还有三个变体,分别面向不同的开发场景:
- docker-compose.local.yml:它才是"应用容器化"的入口,用仓库根目录作为 build context、分别基于 apps/api/Dockerfile、apps/worker/Dockerfile、apps/ws/Dockerfile 构建
api、worker、ws三个服务镜像。也就是说,想用 Docker 镜像而不是pnpm dev跑本地源码时,可借助此文件docker compose -f docker/local/docker-compose.local.yml build。 - docker-compose.agent.yml:面向 Novu Agent 相关开发场景的依赖栈,服务集合与基础版基本一致(localstack、mongo、redis、clickhouse),但没有为 ClickHouse 设置资源配额,Mongo 数据卷也使用
/data/db挂载点。 - docker-compose.e2e.yml:e2e 测试专用,只声明了一个 localstack 服务来模拟 S3,配合 apps 下的 e2e 测试套件(如 apps/api/e2e、apps/webhook/e2e)运行,Mongo、Redis 等在 e2e 场景下通常由测试环境单独提供。
从源码结构可以推断:基础
docker-compose.yml中的 Mongo 卷挂载写为${TMPDIR:-/tmp/mongo}:/db/data,而 agent 变体使用/data/db;如果自定义挂载目录时,请以你实际使用的文件为准。
前置条件与从源码启动本地开发
原文档给出了明确的版本要求:
pnpm 11 的版本要求并非空穴来风:仓库根 package.json 中 setup:project 脚本明确使用了 npx --yes pnpm@11.0.9 i,因此建议在仓库根执行 pnpm --version 确认大版本为 11。
拉起依赖后再装依赖、跑服务
# 1. 启动依赖服务(MongoDB、Redis、LocalStack;可加 clickhouse)
docker compose -f docker/local/docker-compose.yml up -d
# 2. 从仓库根安装项目依赖并构建
npm run setup:project
# 3. 以 portless 模式启动整套应用
pnpm dev:portless
其中两个脚本在 package.json 中的真实定义为:
setup:project:npx --yes pnpm@11.0.9 i && node scripts/setup-env-files.js && pnpm build——安装全部 workspace 依赖、执行 setup-env-files.js 生成各应用所需的.env文件,并完成一次构建;dev:portless:node scripts/mprocs-dev.mjs——通过 mprocs 并行拉起 API、Worker、WebSocket、Dashboard 等应用服务。
完整的本地开发教程见仓库内 docs/community/run-in-local-machine.mdx。按默认配置,Dashboard 开发服务器运行在 http://127.0.0.1:4201(该地址来自原文档;而生产自托管模式下 Dashboard 默认端口是 4000,二者不要混淆)。
密钥配置:绝不能拿默认值上线
原文档给出了一条铁律:
While we provide example secrets for getting started, you should NEVER deploy your Novu setup using the defaults provided.
必改的三个密钥
修改 apps/ 下各服务的 .env 文件(setup:project 已通过 setup-env-files.js 生成),至少以下变量必须换成你自己的随机值:
| 变量 | 作用 | 要求 |
|---|---|---|
JWT_SECRET |
API 生成与校验 JWT 签名 | 加密随机字符串,可用 openssl rand -hex 32 生成 |
STORE_ENCRYPTION_KEY |
加密 / 解密集成商(provider)凭据 | 必须恰好 32 字符,可用 openssl rand -hex 16 生成 |
NOVU_SECRET_KEY |
平台签名密钥(自托管必需的随机密钥) | 加密随机字符串,openssl rand -hex 32 |
第三项 NOVU_SECRET_KEY 虽未在原文档正文出现,但 docker/community/.env.example 与 setup.sh 都把它列为"必须随机生成"的密钥,属于与 JWT_SECRET 同级的安全底线,建议一起修改。
从 setup.sh 的实现看,官方对密钥随机化的处理方式是:
- 用
openssl rand -hex 32生成JWT_SECRET; - 用
openssl rand -hex 16生成STORE_ENCRYPTION_KEY(16 字节十六进制 = 32 个字符,恰好满足"32 characters"要求); - 用
openssl rand -hex 32生成NOVU_SECRET_KEY; - 脚本会先检查
.env中这些键是否为空,为空才写入随机值,随后把.env权限收紧为chmod 600。
Redis TLS 配置
如果连接的是启用 TLS 的 Redis 实例,可在 .env 中加入:
REDIS_TLS={"servername":"localhost"}
REDIS_CACHE_SERVICE_TLS={"servername":"localhost"}
前者作用于业务队列使用的 Redis,后者作用于缓存服务(cache service)使用的 Redis。docker/community/docker-compose.yml 中的 REDIS_HOST / REDIS_PORT / REDIS_PASSWORD / REDIS_CACHE_SERVICE_HOST / REDIS_CACHE_SERVICE_PORT 变量即负责把这两套 Redis 连接信息注入各容器。
面向生产自托管的 docker/community 编排栈
原文档在"Configuration"一节坦承,仓库默认编排为简化做了取舍,生产上并不理想:
- 数据库与应用服务跑在同一台机器上;
- 存储使用 LocalStack 或本地文件系统后端,而不是真正的 S3。
因此文档强烈建议:正式部署前先把数据库与应用解耦。若希望直接得到接近生产的镜像编排,可以参考 docker/community/docker-compose.yml——它定义的正是完整六件套:redis、mongodb、api、worker、ws、dashboard。
社区版栈的服务构成与关键差异
与 docker/local 相比,community 栈有两个显著不同:
- 应用不再是源码进程,而是 ghcr.io 发布的镜像(当前仓库锁定为
3.19.0版本,例如ghcr.io/novuhq/novu/api:3.19.0、worker:3.19.0、ws:3.19.0、dashboard:3.19.0); - MongoDB 开启认证:通过
MONGO_INITDB_ROOT_USERNAME/MONGO_INITDB_ROOT_PASSWORD初始化 root 账号,MONGO_URL也相应带上了?authSource=admin,见 .env.example。
服务间通过 depends_on 的 service_healthy 条件控制启动顺序:api 等 mongodb、redis 健康,dashboard 等 api、worker 健康。所有容器都启用了 json-file 日志驱动并限制为 max-size: 50m、max-file: 5,且 restart: unless-stopped。
默认端口一览
| 服务 | 端口 | 说明 |
|---|---|---|
| API | 3000(${API_PORT:-3000}) |
REST 接口,健康检查命中 /v1/health-check |
| Worker | 3004(${WORKER_PORT:-3004}) |
后台任务处理,健康检查命中 /v1/health-check |
| WebSocket | 3002(${WS_PORT:-3002}) |
实时通道,健康检查命中 /v1/health-check |
| Dashboard | 4000(固定) |
控制台前端,健康检查探测 HTTP 200 |
| MongoDB | 27017 |
仅容器间可达,未发布到宿主机 |
| Redis | 6379 |
仅容器间可达,未发布到宿主机 |
需要重点理解的环境变量
以 .env.example 为准,除三大密钥外还有几组变量直接影响运行方式:
- 运行模式:
NODE_ENV取值可为dev、test、production、ci、local;IS_SELF_HOSTED=true、IS_V2_ENABLED=true、IS_API_IDEMPOTENCY_ENABLED、IS_API_RATE_LIMITING_ENABLED、IS_NEW_MESSAGES_API_RESPONSE_ENABLED等开关控制功能特性; - 连接信息:
MONGO_URL、MONGO_MIN_POOL_SIZE(默认 5)、MONGO_MAX_POOL_SIZE(默认 10)、REDIS_HOST、REDIS_PORT、REDIS_DB_INDEX=2(在 compose 中固定为 2,避免与应用默认 DB 冲突)、REDIS_CACHE_SERVICE_*; - 对象存储:
S3_LOCAL_STACK=http://localhost:4566、S3_BUCKET_NAME=novu-local、S3_REGION=us-east-1以及AWS_ACCESS_KEY_ID=test/AWS_SECRET_ACCESS_KEY=test——即本地仍默认指向 LocalStack,若接入真实 S3 需要整体替换这组变量; - 对外地址:
HOST_NAME=http://localhost、API_ROOT_URL、FRONT_BASE_URL=http://localhost:(4000|4200)、VITE_API_HOSTNAME、VITE_WEBSOCKET_HOSTNAME,它们决定 Dashboard 前端调用 API 与 WebSocket 的目标地址; - 可观测性:
SENTRY_DSN留空;New Relic 默认关闭(NEW_RELIC_ENABLED=false),开启时才需填NEW_RELIC_APP_NAME与NEW_RELIC_LICENSE_KEY; - 队列分片:
BROADCAST_QUEUE_CHUNK_SIZE=100、MULTICAST_QUEUE_CHUNK_SIZE=100控制广播 / 组播任务的批大小。
用 setup.sh 一键自托管
不想手工复制 .env 与生成密钥时,可直接执行 setup.sh:
# 在当前目录就地使用仓库内的 docker-compose.yml 与 .env.example 启动
./docker/community/setup.sh
# 或在空白目录使用(脚本会下载官方 compose 与 env 模板到 ./novu 并启动)
bash setup.sh
脚本行为可以从源码确认:
- 依赖检查:
curl、docker、openssl缺一不可,且要求 Docker Compose v2(检测docker compose version); - 若当前目录已有
docker-compose.yml与.env.example则就地使用,否则下载到./novu; - 生成
.env并自动填充JWT_SECRET、STORE_ENCRYPTION_KEY、NOVU_SECRET_KEY三个随机密钥(对已存在但为空的项做补写),然后chmod 600收紧权限; - 执行
docker compose up -d并提示访问入口:Dashboardhttp://localhost:4000、APIhttp://localhost:3000、WebSockethttp://localhost:3002,日志查看用docker compose logs -f。
更进一步的部署路径
- 原文档中提到的 Helm / Kustomize 入口(
kubernetes/helm/Readme.md)在本仓库快照中未包含kubernetes/目录,Kubernetes 化部署请以社区自托管文档为准; - 仓库内可直接阅读的配套材料有两份:完整的本地开发环境搭建见 docs/community/run-in-local-machine.mdx,Docker 生产化自托管与升级迁移见 docs/community/self-hosting-novu/deploy-with-docker.mdx,其中还涉及 v0 到 v2 的数据迁移 与 遥测配置 等专题。
小结:两套 Compose,一种心智模型
把 docker/Readme.md 与仓库中的实际编排对照后可以得出清晰的结论:docker/local 回答的是"本地开发时数据库与队列从哪来",docker/community 回答的是"生产环境整套系统如何以镜像方式运行"。前者需要配合 Node.js v22.23.0 + pnpm 11.x 与 pnpm dev:portless 从源码启动应用,后者则开箱即用、通过 .env 注入配置。无论哪条路径,JWT_SECRET、STORE_ENCRYPTION_KEY(32 字符)、NOVU_SECRET_KEY 都是必须替换的随机密钥,也是从"能跑"走向"敢上线"的第一步。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00