25 KiB
服务端首次部署操作手册
更新时间:2026-10-22。适用腾讯云 Ubuntu 24.04 / x86_64、1Panel 2.3.2、已有 PostgreSQL 与 doodle_prod 的环境。
用户已确认数据库准备完成;本文不重复建库、初始化表或导入数据,也不表示已现场核验云端数据。本文是操作说明与配套配置,不代表已经部署到云端。
1. 本次采用的部署方式
在 Mac 上构建 Linux 镜像,导出压缩包,通过 1Panel 上传到服务器并导入。服务器只运行构建好的 API,不需要额外安装 Node.js、pnpm、PM2 或源码。暂时无需镜像仓库,后续可改为仓库推送/拉取。
小程序 → https://doodle-api.joeyone.cn
→ 1Panel 已有 OpenResty:443 / 证书
→ doodle-api-prod:8800
→ 1panel-network → 既有 PostgreSQL:5432 / doodle_prod
资源 → https://cdn.joeyone.cn
本阶段只运行一套云端业务环境。小程序 develop 可选择 production 调试;trial/release 固定 production。旧正式版继续访问旧 CloudBase,保留全部旧云函数、预拉取和定时任务;新服务部署成功后先用开发版和体验版验收。
现有数据库由 1Panel 管理;API 由本文 Compose 文件管理,在 1Panel 中可查看其状态和日志。不要再用 1Panel 新建一个同名 API 编排,也不要运行旧 compose.database.yaml。
配套文件都在 doodle-store 仓库:
| 文件 | 作用 |
|---|---|
deploy/Dockerfile.server |
构建 Linux 服务端镜像,启动 dist/main.js |
.dockerignore |
排除本机依赖、真实环境配置和备份 |
deploy/compose.api.yaml |
只启动 API,复用 1Panel 数据库网络 |
deploy/compose.env.example |
镜像版本和网络名模板 |
deploy/server.env.example |
服务运行配置模板,真实值仅在服务器填写 |
首版镜像保留 Prisma CLI 和构建依赖,便于同一个版本镜像执行独立数据库迁移;因此体积尚未优化。迁移不随 API 每次重启自动执行。
2. 服务器部署前检查
以下命令均在 1Panel → 终端 → 本地服务器执行,建议使用当前已有管理终端。不要在 Mac 终端执行服务器命令。
docker version
docker compose version
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
docker network inspect 1panel-network --format '{{range .Containers}}{{println .Name .IPv4Address}}{{end}}'
ss -lntp | rg ':80 |:443 |:8800 '
若服务器没有 rg,最后一条改用 ss -lntp 直接查看。
确认以下条件后继续:
- Docker 正常,Compose 至少 2.30.0;配置使用
env_file.format: raw,低版本会报不支持。版本不足时通过 1Panel 支持的升级方式更新 Compose,再继续。 1Panel-postgresql-RqP6正常运行,属于1panel-network。这是此前核查的名称,若已重装,应以本次实际名称为准,并同步替换server.env。8800未被其他程序占用;80/443继续交给已有 OpenResty。- 在 1Panel → 容器中查出 OpenResty 容器名和网络模式。也可把实际名称代入:
docker inspect <OpenResty容器名> --format '{{.HostConfig.NetworkMode}} {{json .NetworkSettings.Networks}}'
若是 host 模式,反向代理上游使用 http://127.0.0.1:8800;若是 bridge 且与 API 同处 1panel-network,使用 http://doodle-api-prod:8800。若 bridge 未接入该网络,先通过 1Panel 核对并持久配置共享网络,再创建代理;不要把 OpenResty 容器内的 127.0.0.1 当成宿主机。
若不存在 1panel-network,先找到数据库实际所在网络,将该名字写入后面的 .env,而不是盲目新建一个与数据库隔离的同名网络。
检查并备份已有数据库
先在 1Panel 的 PostgreSQL 管理中为 doodle_prod 做一次完整备份,确认任务成功并下载一份到本机。也可通过 DBX 执行以下只读查询核对当前目标和迁移历史:
SELECT current_database(), current_user;
SELECT migration_name, finished_at, rolled_back_at
FROM public._prisma_migrations
ORDER BY migration_name;
SELECT 'categories' AS table_name, count(*) FROM public.categories
UNION ALL SELECT 'worksheets', count(*) FROM public.worksheets
UNION ALL SELECT 'assets', count(*) FROM public.assets;
当前代码包含三份迁移:
0_v35_baseline
20261016000000_category_publication
20261019000000_member_assets
初始化 SQL 已写入这些迁移记录。不要重新运行初始化 SQL、导入本地 _prisma_migrations,也不要删除现有表来绕过迁移错误。
3. 在 Mac 构建和导出镜像
先启动 Docker Desktop。在 Mac 终端执行:
cd /Users/joey-xd/sites/doodle/doodle-store
git status --short
docker version
docker buildx build \
--platform linux/amd64 \
-f deploy/Dockerfile.server \
-t doodle-server:20261022-01 \
--load .
20261022-01 是本文示例版本号。实际每次发布使用新编号,不覆盖上一次可运行的标签;构建包含当前工作区未提交的改动,应先确认这些改动属于本次发布。
必须在 doodle-store 根目录构建,最后的 . 不能省略。即使 Mac 是 Apple Silicon,也显式构建 linux/amd64,与云服务器一致。
Docker 内部安装项目锁定的 Node.js 24.20.0 / pnpm 11.24.0,构建 contracts、生成 Linux Prisma 客户端,再编译 NestJS。不会使用 Mac 的 node_modules,也不需要提供真实数据库密码、微信或七牛密钥来构建。
确认镜像架构并导出:
docker image inspect doodle-server:20261022-01 --format '{{.Os}}/{{.Architecture}}'
mkdir -p /tmp/doodle-release-20261022-01
docker image save doodle-server:20261022-01 | gzip > /tmp/doodle-release-20261022-01/doodle-server-20261022-01.tar.gz
cd /tmp/doodle-release-20261022-01
shasum -a 256 doodle-server-20261022-01.tar.gz > doodle-server-20261022-01.tar.gz.sha256
架构输出应为 linux/amd64。若构建失败,先处理镜像源、依赖下载或编译报错,不上传不完整的压缩包。初次拉取基础镜像和跨架构编译可能较慢。
4. 用 1Panel 上传并导入镜像
服务器建立本次发布目录和长期配置目录。在 1Panel 终端执行:
mkdir -p /opt/doodle/releases/20261022-01
mkdir -p /opt/doodle/prod
chmod 700 /opt/doodle/prod
在 1Panel → 文件中:
- 将 Mac
/tmp/doodle-release-20261022-01/中的压缩包和.sha256文件上传到服务器/opt/doodle/releases/20261022-01/。 - 将仓库
deploy/下的compose.api.yaml、compose.env.example、server.env.example上传到/opt/doodle/prod/。 - 首次在服务器复制模板:
compose.env.example→.env;server.env.example→server.env。后续发布不要再次复制模板覆盖真实配置。
如果文件页隐藏点文件,打开显示隐藏文件后编辑 .env。若通过网页上传大镜像超时,可改用 1Panel 支持的分片上传,或用已有 SSH/SFTP 上传;无需改用浏览器将镜像作为 SQL 文件导入。
在 1Panel 终端执行:
cd /opt/doodle/releases/20261022-01
sha256sum -c doodle-server-20261022-01.tar.gz.sha256
docker image load -i doodle-server-20261022-01.tar.gz
docker image inspect doodle-server:20261022-01 --format '{{.Os}}/{{.Architecture}} {{.Id}}'
校验输出必须为 OK;否则重新上传。docker load 已将镜像导入服务器本机 Docker,后续 Compose 从本机使用该镜像,无需再从镜像仓库下载。
5. 填写服务器配置
5.1 .env:编排配置
编辑服务器 /opt/doodle/prod/.env:
DOODLE_API_IMAGE=doodle-server:20261022-01
PANEL_DOCKER_NETWORK=1panel-network
镜像标签必须与刚导入的一致;网络名必须是数据库实际所在网络。
5.2 server.env:应用配置
编辑 /opt/doodle/prod/server.env。文件采用 raw 格式:每行 KEY=value,不加包裹引号,不写行尾注释,密钥中的 $ 不需要改成 $$。
| 配置项 | 填写要求 |
|---|---|
NODE_ENV / HOST / PORT |
保留 production / 0.0.0.0 / 8800。容器内不能只监听 127.0.0.1 |
JWT_SECRET |
单独生成至少 32 字节的随机密钥;可在自己的终端执行 openssl rand -hex 32,只把结果保存到此文件 |
WECHAT_MINI_APP_ID |
当前小程序 AppID |
WECHAT_MINI_APP_SECRET |
与 AppID 对应的服务端 AppSecret,不能填 JWT 密钥 |
DATABASE_URL |
使用下文格式,指向 doodle_prod |
QINIU_PUBLIC_DOMAIN |
https://cdn.joeyone.cn |
QINIU_BUCKET / QINIU_KEY_PREFIX |
doodle-cn / doodle-assets/ |
QINIU_AK / QINIU_SK |
上传验收前填写真实服务端密钥;只读连通阶段可同时留空 |
QINIU_REGION |
按七牛空间真实区域填写;未知时留空由 SDK 发现,不猜区域 |
SWAGGER_ENABLED |
保留 false,生产默认不开放接口文档 |
CORS_ORIGINS |
仅小程序联调可留空;未来 Web 上线时增加准确 origin,例如 https://doodle.joeyone.cn,不带路径和末尾 / |
TRUSTED_PROXY_CIDRS |
初次留空;确认实际代理来源后再填明确 IP/CIDR,不填 true 或任意来源网段 |
SITE_REFRESH_HOUR_UTC |
22,对应北京时间每日 06:00;新后端任务不会关闭旧云任务 |
数据库连接格式:
postgresql://joey_postgresql:URL编码后的密码@1Panel-postgresql-RqP6:5432/doodle_prod?schema=public
这里的 5432 是 PostgreSQL 容器内部端口;数据库主机不是公网 IP,也不是 API 容器的 127.0.0.1。不需要开放 PostgreSQL 公网端口。
密码中含 @、#、/、% 等字符时必须 URL 编码。可在自己的 Mac 终端交互转换,输入不会回显,结果只用于配置,不发送到聊天或截图:
python3 - <<'PY'
from getpass import getpass
from urllib.parse import quote
print(quote(getpass('数据库原始密码:'), safe=''))
PY
完成后在 服务器终端限制权限并检查编排语法:
cd /opt/doodle/prod
chmod 600 .env server.env
docker compose -f compose.api.yaml config --quiet
检查成功无输出。不要把不带 --quiet 的完整配置输出发到聊天,其中可能包含已展开的密钥。此语法检查不能验证密钥、数据库密码或域名是否正确。
6. 核对迁移状态,再启动 API
以下在 服务器 /opt/doodle/prod 执行:
cd /opt/doodle/prod
docker compose -f compose.api.yaml run --rm --no-deps api pnpm --filter @doodle/server db:status
这个一次性容器使用同样的镜像、环境变量和内部网络,不发布 API 端口,也不会启动 HTTP 服务或业务定时任务。
首次部署且数据库已用当前初始化 SQL 建好时,预期为 Database schema is up to date。若报错,按下表处理:
| 输出 | 处理 |
|---|---|
| 无法连接 / P1001 | 检查网络、数据库容器名和内部端口 |
| 认证失败 / P1000 | 检查账号、密码 URL 编码及库名 |
| 表已存在但没有迁移历史 / P3005 | 停止,不删除表;核对是否执行了完整初始化 SQL,单独制定基线修复方案 |
| 失败迁移 / 校验和不一致 | 停止,核对初始化脚本与本次镜像版本,不手工把失败记录标为成功 |
| 出现待执行迁移 | 首次预期不应出现;确认这些是计划发布的新增迁移并已备份后,再执行下方迁移命令 |
需要应用已审查的新迁移时执行:
docker compose -f compose.api.yaml run --rm --no-deps api pnpm --filter @doodle/server db:deploy
db:deploy 会修改数据库;没有待执行迁移时不会重复初始化。云端不使用 migrate dev、db push 或 migrate reset。
状态确认后启动:
docker compose -f compose.api.yaml up -d --pull never --wait --wait-timeout 120
docker compose -f compose.api.yaml ps
docker compose -f compose.api.yaml logs --tail=100 api
容器名是 doodle-api-prod。API 容器自动重启,启动失败时先看日志,不需要叠加 PM2。健康检查通过只说明 HTTP 和数据库能连接,还需要下一节的业务检查。
7. 先验证服务器内部访问
在服务器终端执行:
curl --fail-with-body http://127.0.0.1:8800/api/v1/health
curl --fail-with-body http://127.0.0.1:8800/api/v1/health/ready
curl --fail-with-body http://127.0.0.1:8800/api/v1/categories
curl --fail-with-body 'http://127.0.0.1:8800/api/v1/worksheets?page=1&pageSize=1'
curl --fail-with-body http://127.0.0.1:8800/api/v1/page-config
前两个预期 HTTP 200 且 code: 0;分类和题型应能查询到已导入并发布的数据。没有数据时检查发布状态、分类可见性、资产 ready 状态和迁移内容,不能仅凭健康检查认定导入成功。
如果 page_configs.current 仍为空或快照引用不正确,需要在管理员功能中执行新后端页面重建,再验收首页;不要重新导入默认占位记录。管理员权限以数据库当前角色为准,不会自动把第一个登录用户设为管理员。
服务启动后会运行新后端的任务逻辑:每日首页刷新受 settings.homeAutoRefresh 控制;配置七牛密钥后还会处理待清理头像。因此启动前应确认导入的任务、资产状态符合 生产数据整合与导入方案,不要把开发库的清理队列直接复制到生产。
8. 配置 API 域名和 HTTPS
8.1 DNS
在 joeyone.cn 的 DNS 管理后台添加:
| 类型 | 主机记录 | 记录值 |
|---|---|---|
| A | doodle-api |
175.24.138.234(以云控制台当前公网 IP 为准) |
DNS 记录只填 doodle-api;1Panel 网站的域名要填完整的 doodle-api.joeyone.cn。证书不代替域名解析。
8.2 1Panel 创建反向代理网站
进入 网站 → 创建网站 → 反向代理:
| 项目 | 值 |
|---|---|
| 域名 | doodle-api.joeyone.cn |
| HTTP 端口 | 80 |
| 代号 | doodle-api |
| HTTPS | 启用,选择包含 joeyone.cn + *.joeyone.cn 的已有证书(此前 ID 11) |
| 代理协议 | http,TLS 在 OpenResty 终止 |
| 代理地址 | host 网络用 127.0.0.1:8800;共享 bridge 网络用 doodle-api-prod:8800 |
| 应用选择 | 自定义 Compose 服务可能不在商店应用列表,手工填写代理地址即可 |
代理路径为 /,保留原始路径和查询参数,不要把上游填写成 /api/v1,避免重复前缀。创建后检查 HTTPS 监听 443、HTTP 跳转 HTTPS,代理缓存关闭。
网站上传大小建议设为 4 MB 或更高,使 2 MiB 图片及 multipart 包装能通过;API 自身仍执行 2 MiB 图片限制。若编辑 OpenResty 配置,在网站对应 server 中设置 client_max_body_size 4m;,不要重复添加同名指令。使用 1Panel 的配置检测和重载功能,不替换其他网站配置。
腾讯云安全组应允许业务访问 80/443。API 的 8800 仅发布到宿主机回环地址,无需放行公网 8800,数据库也不依赖公网 5432。
8.3 公网验收
在 Mac 终端执行,不加 -k 忽略证书验证:
curl --fail-with-body https://doodle-api.joeyone.cn/api/v1/health
curl --fail-with-body https://doodle-api.joeyone.cn/api/v1/health/ready
curl --fail-with-body https://doodle-api.joeyone.cn/api/v1/categories
出现 502 时先回到第 7 节检查 API,再核对 OpenResty 网络和上游。域名根路径 / 返回 404 可以是正常现象,应使用实际健康检查路径。
网站绑定已有证书,并确认其自动续签处于开启状态;未来需要核验续签任务与 OpenResty 实际生效证书,不能只看最初签发成功。
9. 微信与小程序联调
- 微信后台配置新后端调用微信接口所需的实际公网出口 IP 白名单。
10.0.16.14是内网地址,不能填它;若出现微信40164,核对实际出口与白名单。 - 服务器应能出站访问微信接口及七牛服务。检查 AppID/AppSecret 配对,不能用浏览器随便编造一个 code 判断登录是否成功。
- 配置 request、uploadFile 合法域名为
https://doodle-api.joeyone.cn;资源下载使用的 downloadFile 合法域名包含https://cdn.joeyone.cn。依据实际接口用途添加,不在合法域名中填写/api/v1路径。 - API 公网验收成功后,再修改
doodle-mini/miniprogram/config/api.ts中的productionBaseUrl为https://doodle-api.joeyone.cn/api/v1。localBaseUrl保留本地;未部署 staging 时不将其描述为独立可用的测试环境。本手册不会自动修改小程序地址。 - 先在 develop 的 Debug 页切到 production,再用体验版验证真实登录、分类、题型、首页、收藏、下载记录、资料保存、头像上传与管理员发布。trial/release 固定 production,不能通过 Debug 切到本地。
- 使用七牛真实密钥做上传验收前,确认 CloudBase 用户头像绑定、七牛对象 key 和
assets权限映射符合预期,避免把本地测试用户当成线上用户。
只有 HTTP、数据库、真实微信登录和必要业务都验收后,才安排新的正式版发布。旧线上 CloudBase 在并行期仍产生新数据;部署本身不会实现双向同步,正式切换前还需按迁移方案安排最新补导和对账。
10. 日常维护、更新和回退
查看状态与重新加载配置
在服务器 /opt/doodle/prod:
docker compose -f compose.api.yaml ps
docker compose -f compose.api.yaml logs --tail=100 api
docker stats --no-stream doodle-api-prod
修改 server.env 后必须重建容器来注入新配置,仅 restart 不会读取新环境变量:
docker compose -f compose.api.yaml up -d --pull never --force-recreate --wait --wait-timeout 120
Compose 的内存限制为 768 MiB,Node 堆上限为 384 MiB。上线后根据实际占用调整;不要为小项目预先启动第二套 API/数据库占满服务器。unhealthy 不会自动触发 Docker 重启,需查看告警/日志和实际进程;restart: unless-stopped 处理的是进程退出。
发布新版本
- 在本机使用新标签重新构建,保存 Git 提交/工作区说明、构建输出和镜像 ID。
- 导出、校验、上传、
docker load;保留旧镜像与旧配置副本。配置副本含密钥,仅存权限受限目录,不能打入发布包。 - 完整备份数据库并确认可下载恢复;查看本次迁移 SQL,判断旧程序能否兼容新结构。
- 编辑
.env的DOODLE_API_IMAGE为新标签,先运行db:status;需要时执行db:deploy,再up -d --pull never --wait --wait-timeout 120。 - 重做内部和公网健康、业务检查,以及涉及本次改动的小程序验收。
当前是单 API 容器更新,重建期间会有短暂中断;若以后需要零中断,再设计双实例切换。本手册不宣称零停机。
应用回退
若数据库结构仍兼容旧镜像,把 .env 标签改回上一版本,恢复匹配的运行配置,再执行:
docker compose -f compose.api.yaml up -d --pull never --wait --wait-timeout 120
应用回退不会自动撤销数据库迁移。若新增迁移不兼容旧程序,先停止业务写入并制定恢复方案,不能直接执行 migrate reset;恢复旧备份还会丢失备份之后的新写入,必须先保留当前数据。
定期备份
通过 1Panel 给 doodle_prod 配置每日完整备份、保留周期和服务器外副本,并在独立恢复库演练恢复。网站备份不能自动等同于 PostgreSQL 备份;仅删除/更新 API 容器不会删除数据库,但服务器损坏仍会同时影响本机文件和容器卷。
11. 常见故障
| 现象 | 排查方式 |
|---|---|
env_file ... format 不支持 |
Compose 升级到 2.30.0 及以上 |
| 镜像不存在 / pull access denied | 检查是否执行 docker load,以及 .env 标签是否一致;离线发布使用 --pull never |
exec format error |
镜像架构错误,重建 linux/amd64 |
| 启动即退出 | 检查 JWT 长度、必填微信凭据、连接串格式及日志;不要贴出真实环境文件 |
| health 成功、ready 失败 | 检查 PostgreSQL 网络、密码、数据库名;health 不检查数据库 |
| ready 成功、业务查询失败 | 检查表结构、迁移历史、导入数据与资产状态 |
| 502 Bad Gateway | 检查 API 是否 healthy、OpenResty 网络与上游地址 |
| 413 Request Entity Too Large | 检查 OpenResty 上传上限及后端图片 2 MiB 限制 |
微信 40164 |
服务端实际出口 IP 不在微信白名单 |
| 登录正常、上传失败 | 检查七牛 AK/SK、空间区域、上传域名及资产权限 |
| 首页为空 | 检查内容发布状态和页面快照;通过有权限的管理员重建 |
| 修改配置后仍用旧值 | up --force-recreate,而不是仅 restart |
12. 本轮验证范围
2026-10-22 已在本机验证 Compose 解析、单 API 服务、回环端口绑定、已有外部网络引用及 raw 环境变量特殊字符处理;文档和 YAML 格式检查通过。
实际尝试构建 linux/amd64 镜像时,Docker Hub 的 node:24.20.0-bookworm-slim 元数据下载超时,尚未进入依赖安装和编译。因此镜像完整构建、容器启动和迁移 CLI 仍需在网络恢复后验证,不能把本手册当作已部署成功的记录。云端没有执行迁移、启动 API 或修改小程序接口地址。