部署指南
推荐使用 Docker Compose 部署。一套 compose 包含两个服务:PostgreSQL(db)和 Node 应用(app)。应用进程同时提供后端接口和构建好的前端页面。
应用不做自动部署
应用不做 CI 自动部署。.github/workflows/ci.yml 只在推送到 main 和 Pull Request 时运行 lint、类型检查、测试、门禁和前端构建。部署在服务器上手动完成,更新流程见下文。
文档站是例外:.github/workflows/docs.yml 在 website/ 有改动合入 main 时自动构建并发布到 GitHub Pages(需在仓库 Settings → Pages 中把 Source 设为 GitHub Actions),Pull Request 只构建、检查死链。
架构概览
| 组件 | 说明 |
|---|---|
db | 镜像 postgres:alpine,库名和用户名均为 castor_kit,数据存放在卷 postgres_data |
app | 由仓库根目录的 Dockerfile 构建,容器内监听 5000 端口,上传文件存放在卷 app_data(挂载到 /app/data) |
镜像构建分两个阶段:两个阶段都基于 node:22-bookworm-slim(glibc;sodium-native 等原生模块只提供 glibc 版预编译文件,不能用 Alpine)。第一阶段安装依赖并构建前端(Vite)和后端(tsup),再裁剪为生产依赖;第二阶段是运行镜像,以非 root 用户(uid 10001)运行,并配置了基于 /health 的健康检查。
容器启动时,docker-entrypoint.sh 依次执行:
node dist/setup-once.js:在 PostgreSQL advisory lock 保护下执行数据库迁移、RBAC 增量同步、创建或更新 AI SQL 只读账号castor_kit_ro;演示模式下还会在到期时恢复演示数据。多个副本同时启动时也只会依次执行,结果幂等。node dist/main.js:启动服务。
构建时使用的镜像源
Dockerfile 从构建参数 NPM_REGISTRY 指定的源安装依赖,默认为 https://registry.npmjs.org;docker-compose.yml 传入的是国内镜像 https://registry.npmmirror.com,所以通过 compose 构建时默认使用该镜像。要换成其他源,在 .env.production 中设置 NPM_REGISTRY,例如 NPM_REGISTRY=https://registry.npmjs.org。
方式一:安装向导
git clone https://github.com/robeshell/castorjs.git
cd castorjs
bash scripts/setup.sh向导会询问管理员密码、访问端口(默认 5000)以及可选的 AI 配置,随机生成 SECRET_KEY、POSTGRES_PASSWORD、POSTGRES_RO_PASSWORD,写入 .env.production,然后构建并启动服务,等待 /health 就绪。
脚本不会修改你的 Docker 设置。如果所在网络拉取镜像或安装依赖失败,请自行在 Docker 中配置镜像加速并设置 NPM_REGISTRY(见上方镜像源说明),然后重新运行。
方式二:手动配置
1. 创建 .env.production
在仓库根目录创建 .env.production,至少包含以下变量(compose 缺少任何一项都会拒绝启动):
SECRET_KEY=<足够长的随机字符串>
ADMIN_PASSWORD=<admin 账号的初始密码>
POSTGRES_PASSWORD=<数据库密码>
POSTGRES_RO_PASSWORD=<AI SQL 只读账号密码>可选变量:
APP_PORT=5000 # 宿主机端口,不设置时为 8080
NPM_REGISTRY=https://registry.npmjs.org # 构建镜像时使用的 npm 源;compose 默认用 https://registry.npmmirror.com邮件、文件存储、上传限制、AI 模型不用写在这里:部署后登录,在「系统设置」页面里配置即可。想用环境变量锁定某一项时,见 系统设置里的配置。全部可用变量见 配置项。随机字符串可以用 openssl rand -base64 48 生成。
2. 构建并启动
docker compose --env-file .env.production up -d --build首次构建需要几分钟。之后访问 http://<服务器地址>:<APP_PORT>,使用 admin 和 ADMIN_PASSWORD 登录。
每条 compose 命令都要带 --env-file
compose 默认只读取 .env,不会读取 .env.production。不带 --env-file .env.production 时,必填变量缺失,命令会直接报错。
ADMIN_PASSWORD 只在首次生效
admin 账号只在不存在时创建。首次启动后再修改 ADMIN_PASSWORD 不会改变已有账号的密码,请登录后在界面上修改,或 重置密码。
方式三:Render + Neon(免费演示)
用 Render 的免费 Web 服务运行应用、Neon 的免费 PostgreSQL 存数据,适合搭一个公开的在线演示。仓库根目录的 render.yaml 已写好配置,默认开启演示模式:
- 登录页显示演示账号(
admin/castor-demo),可以一键登录 - 系统管理只读,不能改密码;组件示例可以随意增删改
- 示例数据每 24 小时自动恢复
免费套餐的限制
以下是撰写时两家平台的免费额度,开通前请以官网为准:
- Render 免费实例 15 分钟无人访问会休眠,再次访问需要等待几十秒启动;休眠期间定时任务不运行
- Render 免费实例的磁盘在重启或休眠后会清空:默认的
local存储驱动保存的上传文件(组件示例里上传的图片、附件)会随之丢失。演示数据本来就会定期恢复,所以render.yaml保持local,并把单个文件上限设为 2MB;需要保留文件时,在 Render 的 Environment 中加上STORAGE_DRIVER=s3和一个 Cloudflare R2 桶的S3_*配置(演示模式下系统设置页只读,所以这里用环境变量,见文件存储与上传) - Neon 免费数据库空闲时会暂停计算,下次连接时自动唤醒
1. 创建 Neon 数据库
- 注册 Neon,新建一个项目。区域选 AWS US East 2 (Ohio),与
render.yaml里 Render 服务的region: ohio一致;若改用其他区域,两边保持一致 - 在项目首页点 Connect,关闭「Connection pooling」,复制直连的连接串,形如
postgresql://<用户>:<密码>@ep-xxx.<区域>.aws.neon.tech/neondb?sslmode=require。主机名里不能带-pooler:带了的话 AI 数据查询会报错,把-pooler删掉即可
为什么要直连
启动时的初始化(迁移、RBAC 同步、演示数据恢复)使用会话级的 advisory lock 防止并发,连接池模式下拿不到这把锁。连接串里的 channel_binding=require 可以保留,也可以去掉。
2. 在 Render 上部署
- 用 GitHub 账号注册 Render。如果仓库不在你的账号下,先 Fork 一份
- 在 Render 控制台选择 New → Blueprint,选中仓库,Render 会读取
render.yaml - 按提示填入
DATABASE_URL(上一步复制的连接串)。Render 还会询问AI_API_KEY和AI_MODEL,可以先留空,需要 AI 时再设置(见下文第 4 步)。其余变量已在render.yaml中设置或自动生成 - 点 Apply。首次构建大约需要 5–10 分钟,状态变成 Live 后打开服务地址(
https://<服务名>.onrender.com),登录页就能看到演示账号
也可以直接点击 README 中的 Deploy to Render 按钮,效果相同。
3. 之后的维护
render.yaml没有关闭自动部署:推送到 main 后 Render 会自动重新构建,构建失败会发邮件通知。不需要时可以在服务的 Settings → Build & Deploy 里关闭 Auto-Deploy- 演示账号的密码是
render.yaml里的ADMIN_PASSWORD,只在首次初始化、账号还不存在时生效,要改请在第一次部署前修改 - 手动立即恢复演示数据:在本地检出的仓库中带上 Neon 连接串执行
pnpm demo:reset,例如DEV_DATABASE_URL='<Neon 连接串>' pnpm demo:reset(脚本使用当前NODE_ENV对应的数据库,开发环境下就是DEV_DATABASE_URL)。付费的 Render 实例也可以在服务的 Shell 中执行node dist/demo-reset.js;免费实例没有 Shell
4. 接入 AI(可选)
演示站可以接入 Google Gemini 的免费额度来演示 AI 对话和 AI 数据查询:
- 在 Google AI Studio 用 Google 账号创建 API Key
- 在 Render 服务的 Environment 中设置
AI_API_KEY(上一步的 key)和AI_MODEL(推荐gemini-3.5-flash);AI_API_BASE已在render.yaml中设为 Gemini 的 OpenAI 兼容地址。保存后服务会自动重启
模型选择
最新发布的 Flash 模型在免费档上经常因为负载过高返回 503(例如写作本文时的 gemini-3.8-flash)。演示环境建议用发布较早的稳定版,如 gemini-3.5-flash 或 gemini-3.5-flash-lite。遇到报错时,在 Render 的 Logs 中搜索「AI 上游返回错误」可以看到上游返回的原因。
演示模式会限制 AI 调用:每个 IP 每小时 20 次、全站每天 300 次、单次输入最多 4000 字符,并限制回复长度,可用 DEMO_AI_* 变量调整(见配置项)。免费档的请求数据可能被服务商用于改进产品,演示环境不要输入敏感信息。
启动时报错无法创建只读账号
启动时会创建 AI 数据查询用的只读账号 castor_kit_ro。如果 Neon 拒绝创建,在 Neon 的 SQL Editor 中手动执行:
CREATE ROLE castor_kit_ro LOGIN PASSWORD '<Render 中 POSTGRES_RO_PASSWORD 的值>';然后在 Render 中重新部署。初始化会为已存在的账号更新密码并授权。
用 Render 部署正式环境
把 DEMO_MODE 改为 false,并把 ADMIN_PASSWORD 换成强密码即可。但免费实例会休眠、定时任务不会按时运行、磁盘会被清空(上传文件必须改用 s3 驱动),正式使用建议选择付费实例,或用方式一、方式二部署到自己的服务器。
常用运维命令
docker compose --env-file .env.production ps # 查看服务状态
docker compose --env-file .env.production logs -f app # 查看应用日志
docker compose --env-file .env.production restart app # 重启应用
docker compose --env-file .env.production down # 停止服务,保留数据卷
curl -f http://localhost:<APP_PORT>/health # 健康检查/health 在数据库可用时返回 { status: 'healthy', ... },否则返回 500。
不要随意使用 down -v
docker compose down -v 会删除数据卷,数据库和上传文件都会丢失。
重置管理员密码
要给 admin 账号设置新密码(例如忘记了密码),先在 .env.production 的 ADMIN_PASSWORD 里写入新密码,重建容器让它读到新值,再在容器里运行重置脚本:
docker compose --env-file .env.production up -d
docker compose --env-file .env.production exec app node dist/reset-admin-password.js它只修改 admin 的密码(并确保该账号有超级管理员角色),其他账号不受影响。如果因为多次失败登录被锁定,需要等锁定结束(默认 15 分钟)。不用 Docker 时,在 API 目录下以生产环境运行 node dist/reset-admin-password.js,或在源码目录运行 pnpm seed:rbac -- --incremental --reset-admin-password。
更新流程
git pull
docker compose --env-file .env.production up -d --buildcompose 会用最新代码重新构建镜像并重建 app 容器。容器启动时自动执行新的数据库迁移和 RBAC 增量同步:新菜单自动出现并授予超级管理员,已有的用户、角色和自定义数据不会被清除。
建议在更新前备份数据库,见下文。
数据持久化与备份
| 卷 | 默认名称 | 内容 |
|---|---|---|
postgres_data | castor-kit_postgres_data | PostgreSQL 数据 |
app_data | castor-kit_app_data | 上传文件(local 存储驱动;用 s3 驱动时文件在对象存储里) |
卷名可在 .env.production 中用 COMPOSE_DB_VOLUME / COMPOSE_DATA_VOLUME 覆盖,例如指向已有的卷。
备份数据库示例:
docker compose --env-file .env.production exec db pg_dump -U castor_kit castor_kit > castor_kit_backup.sql固定 PostgreSQL 版本
docker-compose.yml 中 db 使用的镜像标签是 postgres:alpine,在新机器上拉取时会得到最新的主版本。PostgreSQL 的数据目录不能跨主版本直接使用,生产环境建议把标签改为固定的主版本。
反向代理与 HTTPS
生产环境建议在应用前面放一层反向代理(如 Nginx)处理 TLS。需要注意:
- 转发
Host和协议头:应用信任一跳代理,会从X-Forwarded-For/X-Forwarded-Proto获取客户端 IP 和协议。COOKIE_SECURE留空时,按请求协议自动决定是否给 cookie 加Secure标志,所以要正确传递X-Forwarded-Proto。 - WebSocket:
/ws路径需要转发Upgrade头。WebSocket 握手会校验Origin与Host同源(或在CORS_ORIGINS白名单中),因此代理必须保留原始Host。 - 请求体大小:应用允许的请求体上限默认为 16MB(
BODY_LIMIT),导入文件上限为 5MB。Nginx 的client_max_body_size默认只有 1MB,需要相应调大。 - 流式响应:AI 对话使用 SSE,应用已在响应头中设置
X-Accel-Buffering: no关闭 Nginx 缓冲。
Nginx 配置示例(假设 APP_PORT=5000):
server {
listen 80;
server_name example.com;
client_max_body_size 16m;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}HTTPS 证书可以使用 Let's Encrypt(例如 Certbot 的 Nginx 插件)申请。启用 HTTPS 后,也可以在 .env.production 中显式设置 COOKIE_SECURE=true。
只通过代理访问
使用反向代理时,可以把 docker-compose.yml 中的端口映射改为只绑定本机(如 "127.0.0.1:${APP_PORT:-8080}:5000"),避免绕过代理直接访问。
多副本与定时任务
默认是单个 app 容器,定时任务调度器在 web 进程内运行(compose 中 RUN_SCHEDULER_IN_WEB 默认为 true)。
需要多个应用副本时:
- 给 web 副本设置
RUN_SCHEDULER_IN_WEB=false。 - 另外运行一个调度进程:使用同一镜像,覆盖 entrypoint(而不是
command)为node dist/worker.js。
镜像的 ENTRYPOINT 是 docker-entrypoint.sh,它总是执行 setup-once 后启动 main.js,不会读取 command。因此在 docker-compose.yml 中添加调度服务时写成:
worker:
build: .
restart: unless-stopped
entrypoint: ["node", "dist/worker.js"]
environment:
# Same variables as the app service (DATABASE_URL, SECRET_KEY, ADMIN_PASSWORD, AI_SQL_DATABASE_URL, ...)
depends_on:
db:
condition: service_healthy调度服务不执行 setup-once,数据库初始化仍由 app 容器完成。
调度器基于数据库租约,同一个任务同一时间只会被一个进程领取,即使多个进程同时运行调度也不会重复执行。setup-once 使用 advisory lock,多个副本同时启动也是安全的。
适用范围与进程内状态
Castor 面向中小型业务系统(管理后台、内部工具、B2B 控制台),只依赖 PostgreSQL,不需要 Redis。会话、登录锁定、定时任务和 webhook 重试都存放在数据库中,所有副本看到的是同一份状态。大多数部署用一个 app 容器就够了;为了高可用或更多 CPU 增加副本时,先确认以下几点:
- 限流按进程计数。 N 个副本放在负载均衡后面时,实际上限最多是系统设置中数值的 N 倍。登录锁定在数据库中计数,不受影响。
- 本地文件存储需要共享卷。 使用
local存储驱动时,所有副本必须挂载同一个/app/data卷,否则请在系统设置中改用 S3。 - 所有副本使用同一个
SECRET_KEY,否则一个副本签发的会话 cookie 会被其他副本拒绝。 - 性能监控页面显示的是浏览器当前连接到的那个副本。
不使用 Docker 部署
需要 Node 22+、pnpm 和 PostgreSQL 14+。
# 1. 安装依赖并构建
corepack enable
pnpm install --frozen-lockfile
pnpm build
# 2. 在仓库根目录或 apps/api/ 下创建 .env.production,至少包含:
# DATABASE_URL、SECRET_KEY、ADMIN_PASSWORD、POSTGRES_RO_PASSWORD
# 3. 初始化数据库(迁移 + RBAC 增量同步 + 只读账号)
NODE_ENV=production node apps/api/dist/setup-once.js
# 4. 启动服务(默认监听 0.0.0.0:5000,可用 PORT 修改)
NODE_ENV=production node apps/api/dist/main.jsNODE_ENV必须在命令行(或进程管理工具)中设置,后端据此决定加载.env.production。- 第 3 步会用
POSTGRES_RO_PASSWORD创建只读账号castor_kit_ro,所以DATABASE_URL中的账号需要有创建角色的权限。未设置AI_SQL_DATABASE_URL时,AI 数据查询使用把DATABASE_URL换成该账号后的连接;只有想用其他连接时才需要设置AI_SQL_DATABASE_URL,并让它指向只读账号,例如postgresql://castor_kit_ro:<POSTGRES_RO_PASSWORD>@<host>/<库名>。 - 前端构建产物在
apps/web/dist/,后端默认从这里提供页面。 - 建议用 systemd、pm2 等进程管理工具托管
main.js;独立调度进程为apps/api/dist/worker.js。 - 更新时:
git pull→pnpm install --frozen-lockfile→pnpm build→ 再次执行第 3 步 → 重启服务。
