Skip to content

Docker 部署

Docker Compose 会启动 PostgreSQL、Redis、一次性迁移、API、worker 与 Nginx,适合生产试运行与中小规模部署。配置文件位于仓库根目录:Dockerfiledocker-compose.ymldocker-compose.dev.yml.env.docker

前置依赖

  • Docker 24+
  • Docker Compose v2

快速开始

bash
git clone https://github.com/iwangbowen/zenith-admin.git
cd zenith-admin

cp .env.docker .env
# 一次生成四个必填项并填入 .env(JWT_SECRET / FIELD_ENCRYPTION_KEY / POSTGRES_PASSWORD / REDIS_PASSWORD 均无默认值,留空无法启动)
npm run secret:generate -- --docker

docker compose up -d

# 首次部署或需要补齐内置数据时执行,可重复运行(复用 migrate 服务的一次性容器)
docker compose run --rm migrate node dist/db/seed.js

docker compose ps

宿主机端口与暴露面

Compose 默认只对外映射 80(Web);API 端口 3300 默认绑定宿主机回环 127.0.0.1(浏览器与 WebSocket 经 Nginx 同源代理 /api),PostgreSQL / Redis 不映射宿主机端口——Docker 端口映射会绕过 ufw / firewalld,直接映射等于把数据库和会话存储暴露到公网。 端口冲突时在 .env 中调整 WEB_PORT / API_PORT;确需直接对外暴露 API 时设置 API_BIND=0.0.0.0。本机排障需要 psql / redis-cli 直连时叠加排障文件:docker compose -f docker-compose.yml -f docker-compose.debug.yml up -d(绑定 127.0.0.1,端口由 POSTGRES_PORT / REDIS_PORT 控制)。

访问地址:

服务默认地址
前端 / Nginxhttp://localhost
APIhttp://localhost/api(直连 http://127.0.0.1:3300 仅本机)
Mastra Studiohttp://localhost/studio/

默认管理员:admin / 123456

TIP

迁移由一次性 migrate 服务执行,api / worker 会等待其成功完成后启动;需要手动补迁移时执行 docker compose run --rm migrate。种子数据需手动执行一次,可重复执行。

服务拓扑

text
postgres ─┐
redis    ─┤──→ migrate(一次性迁移)
          ├──→ api × N (ZENITH_ROLES=api, Node.js :3300) ──→ web (Nginx :80)
          └──→ worker × M (ZENITH_ROLES=worker, probe :3301)

api / worker → Redis pub/sub → api  (WebSocket / IoT 推送扇出)
api ⇄ api_storage ⇄ worker       (本地文件、上传暂存、CMS 静态产物)
服务镜像 / 阶段说明
postgrespostgres:16-alpine数据库,库名 zenith_admin;Compose 启动时预加载 pg_stat_statements
redisredis:7-alpine会话、限流、幂等、黑名单与 WS 扇出状态;始终 requirepass + AOF
migrateDockerfile server stage一次性执行 node dist/db/migrate.jsrestart: "no"
apiDockerfile server stageHono 后端,ZENITH_ROLES=api,端口 3300,健康检查 /api/health;以非 root 用户 node 运行
workerDockerfile server stage后台任务进程,ZENITH_ROLES=worker,健康检查 http://localhost:3301/health,默认不发布端口
webDockerfile web stageNginx 静态站点,代理 /api/api/ws

Dockerfile 构建流程

阶段基础镜像行为
buildernode:24-alpine安装全量依赖,构建 shared、analytics-sdk、server(含 PDF 字体子集生成)、web,按 PDF_FONT 构建参数用 packages/server/scripts/package-server.mjs 组装 server 部署目录,执行 docker/build-studio.mjs,最后用 docker/patch-shared-exports.mjs@zenith/shared 的 exports 指向编译产物
servernode:24-alpine安装生产依赖,复制组装好的 server dist、Drizzle 迁移、PDF 字体与 shared dist,写入 entrypoint;entrypoint 传入参数时直接执行该命令(migrate 服务据此运行 node dist/db/migrate.js),否则启动 node dist/index.jsstorage / logs 归属 node 后切换 USER node
webnginx:1.30-alpine复制 packages/web/distdocker/nginx.conf

node-pty 在 Linux 下需要编译,构建阶段安装 python3 make g++;server 阶段保留 libstdc++ 并移除编译工具链。

为什么产物可以用纯 Node 运行?

源码中的相对导入不带扩展名(依赖 tsx / Vite 解析),而 Node.js 原生 ESM 要求显式 .js 扩展名。 shared 与 server 的 build 脚本在 tsc 之后运行 tsc-alias --resolve-full-paths,把 dist 中的相对导入改写为完整路径; docker/patch-shared-exports.mjs 再把 @zenith/sharedexports./src/*.ts 机械改写为 ./dist/*.js。 两步之后 node dist/db/migrate.jsnode dist/index.jsnode dist/db/seed.js 均可脱离 tsx 直接运行。

环境变量

变量默认值说明
JWT_SECRET无(必填)JWT 签名密钥,≥ 32 字符随机值,按服务实例独立;npm run secret:generate 生成
FIELD_ENCRYPTION_KEY无(必填)字段级 AES-256-GCM 密钥(64 位 hex),按数据库共享——连同一个库的实例必须一致,轮换会使已入库密文不可读
POSTGRES_PASSWORD无(必填)PostgreSQL 口令;npm run secret:generate -- --docker 生成
REDIS_PASSWORD无(必填)Redis 口令,始终启用 requirepass;会拼进 REDIS_URL,只能用 URL 安全字符(生成器输出即满足)
REDIS_URLredis://:${REDIS_PASSWORD}@redis:6379/0API 使用的 Redis URL,可整体覆盖为外部 Redis
WEB_PORT80Nginx 对外端口
API_PORT3300API 宿主机端口
API_BIND127.0.0.1API 端口绑定地址;0.0.0.0 才对外暴露
POSTGRES_PORT / REDIS_PORT5432 / 6379仅在叠加 docker-compose.debug.yml 时生效,绑定 127.0.0.1
ALLOWED_ORIGINSCSRF 允许来源
CORS_ORIGIN*CORS 允许来源
LOG_LEVELinfo后端日志级别
OAUTH_GITHUB_CLIENT_ID / OAUTH_GITHUB_CLIENT_SECRETGitHub OAuth 登录凭据
OAUTH_CALLBACK_BASE_URLhttp://localhostOAuth 回调基础地址
TAGlatest本地构建镜像标签
PDF_FONTsubset构建参数:镜像携带的 PDF 导出字体规格。subset 为 Noto Sans SC 子集(约 2.5MB,GB 2312 ∪ 通用规范汉字表 + 常用符号),full 为全量(约 8MB,含繁体 / 生僻字);改后需 docker compose build
WORKER_SHUTDOWN_GRACE_MS120000worker 优雅停机硬截止(同时作为 pg-boss 等待在飞作业收尾的预算);Compose 为 worker 设 stop_grace_period: 130s、为 api 设 25s(api 硬截止 15s),均须大于对应进程的硬截止

JWT_SECRET / FIELD_ENCRYPTION_KEY / POSTGRES_PASSWORD / REDIS_PASSWORD 任一留空时 docker compose up 直接失败(前两者为占位值时 API 启动也会失败)。生产环境请按实际域名设置 ALLOWED_ORIGINS。Compose 已固定 api / worker 的 ZENITH_ROLES,通常无需在 .env 中覆盖。使用外部 Redis 时整体覆盖 REDIS_URL(含口令)即可。

API 容器以非 root 用户 node 运行;如需在容器内访问宿主机 Docker socket(运维模块的容器管理),请在自定义 override 中挂载 socket 并通过 group_add 加入 socket 所属组,不要改回 root。

镜像内随包携带 PDF 导出所需的 CJK 字体(Noto Sans SC,SIL OFL),报表打印与审批单的 PDF 中文渲染无需再安装系统字体。默认是约 2.5MB 的子集(PDF_FONT=subset),含繁体 / 生僻字的场景用 docker build --build-arg PDF_FONT=full --target server .(compose 用户在 .envPDF_FONT=full 后重新 build)切换为约 8MB 的全量字体;企业自有字体挂载进容器后以 REPORT_PDF_FONT_PATH 指定即可覆盖。两种规格的覆盖范围与启动 / 导出日志判读见部署说明 → PDF 字体

Nginx 行为

docker/nginx.conf 的当前行为:

  • /api 代理到 api:3300,包含 WebSocket upgrade,覆盖 /api/ws
  • /studio/ 托管 Mastra Studio 静态资源,数据面走同源 /api/mastra
  • /studio/refresh-events 返回 204,减少静态部署下的 EventSource 重试日志。
  • /index / /index.html 跳转到 /
  • / fallback 到 /index.html 支持 SPA 路由。
  • 所有 HTML 入口(/member.htmlapproval.html)下发 Cache-Control: no-cache, must-revalidate,浏览器每次校验后拿到最新的 hash 资源清单,发版不会留在旧壳层。
  • JS/CSS/字体/图片等静态资源使用一年 immutable 缓存。
  • gzip_static on:直接下发构建期预生成的 .gz 文件(packages/web/scripts/precompress.mjs,gzip level 9);未预压缩的响应按 gzip_comp_level 6 动态压缩。构建同时产出 .br,官方 nginx:1.30-alpine 不含 brotli 模块,如自建含 ngx_brotli 的镜像可打开配置里注释的 brotli_static on
  • 容器内 listen 80 只提供 HTTP/1.1。多入口 SPA 的资源请求并发度依赖 HTTP/2,生产环境应在 TLS 终结层(外层反向代理或本镜像挂证书后 listen 443 ssl; http2 on;)启用 HTTP/2,否则首屏请求会受 6 连接/域名限制排队。
  • 安全响应头:所有响应下发 X-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin,并以 X-Frame-Options: SAMEORIGIN + Content-Security-Policy: frame-ancestors 'self' 禁止跨站嵌入;仅 /public/report/(公开仪表盘嵌入)不下发帧保护头,嵌入来源由应用按仪表盘 embed.allowedOrigins 校验。SPA 的内容安全策略(script-src 不含 'unsafe-inline' 等)由 Vite 构建期注入入口 HTML 的 <meta http-equiv="Content-Security-Policy"> 承担(见 packages/web/vite.config.ts),nginx 无需重复配置。

常用操作

bash
# 查看日志
docker compose logs -f
docker compose logs -f api
docker compose logs -f worker
docker compose logs -f web

# 停止服务(保留数据卷)
docker compose down

# 停止并删除数据卷
docker compose down -v

# 扩容 / 缩容
docker compose up -d --scale api=2 --scale worker=3

# 手动执行迁移
docker compose run --rm migrate

# 单容器 all-in-one 覆盖文件(worker 副本数为 0)
docker compose -f docker-compose.yml -f docker-compose.single.yml up -d

# 进入容器
docker compose exec api sh

# 连接数据库
docker compose exec postgres psql -U postgres -d zenith_admin

升级版本

bash
git pull
docker compose build --no-cache
docker compose up -d

migrate 服务会先运行一次迁移,apiworker 通过 depends_on: condition: service_completed_successfully 等待迁移完成后再启动。前端静态资源由 web 镜像提供,重建镜像后随容器替换生效。需要单独补迁移时运行 docker compose run --rm migrate

本地开发基础设施

bash
docker compose -f docker-compose.dev.yml up -d
npm run dev

docker-compose.dev.yml 只启动 postgres:16-alpineredis:7-alpine,端口固定映射为 5432 / 6379,用于配合本地 Node / Vite 开发。PostgreSQL 服务会在启动时预加载 pg_stat_statements,因此本地 SQL 监控可直接使用;已有容器需要重建或执行 docker compose -f docker-compose.dev.yml up -d --force-recreate postgres 才会应用新的启动参数。

仓库 Compose 文件只负责配置自带的 PostgreSQL 容器。若使用外部或托管 PostgreSQL,需要由数据库运维配置 shared_preload_libraries = 'pg_stat_statements' 并重启数据库,迁移再负责创建数据库扩展。

数据持久化

卷名内容
postgres_dataPostgreSQL 数据
redis_dataRedis AOF 数据
api_storagestorage/ 整体,同时挂载到 api 与 worker:本地上传文件、分片上传暂存目录、CMS 静态化产物(worker 写、api 读)
api_logsapi 角色日志
worker_logsworker 角色日志
bash
# 备份 PostgreSQL
docker compose exec postgres pg_dump -U postgres zenith_admin > backup.sql

# 恢复 PostgreSQL
docker compose exec -T postgres psql -U postgres zenith_admin < backup.sql

Built with VitePress for local documentation preview.