diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..953ff83 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +# 构建产物必须在目标 Linux 平台重新生成。 +**/node_modules +**/.pnpm-store +**/dist +**/dist-test +**/coverage +**/*.tsbuildinfo +apps/server/src/generated +**/.git +**/.DS_Store +**/*.log +# 真实密钥、备份与资源报告禁止进入镜像;仅保留示例配置。 +**/.env +**/.env.* +!**/.env.example +**/server.env +**/server.env.* +!**/server.env.example +deploy/data +deploy/backups +**/*.dump +**/*.tar +**/*.tar.gz +packages/asset-sync/reports +.agents +.codex +docs diff --git a/.gitignore b/.gitignore index 241f571..e26e4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ coverage/ !.env.example /deploy/data/ /deploy/backups/ +/deploy/server.env +/deploy/server.env.* +!/deploy/server.env.example diff --git a/deploy/Dockerfile.server b/deploy/Dockerfile.server new file mode 100644 index 0000000..aec3fac --- /dev/null +++ b/deploy/Dockerfile.server @@ -0,0 +1,21 @@ +# 在 Linux 内安装依赖和生成 Prisma 客户端,不能复用开发机的 node_modules。 +FROM node:24.20.0-bookworm-slim AS base +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates openssl \ + && rm -rf /var/lib/apt/lists/* \ + && npm install --global pnpm@11.24.0 +WORKDIR /app + +FROM base AS build +COPY . . +RUN pnpm install --frozen-lockfile --filter @doodle/server... +RUN pnpm --filter @doodle/server... run build + +FROM base AS runtime +ENV NODE_ENV=production HOST=0.0.0.0 PORT=8800 +# 首版保留构建依赖及 Prisma CLI,允许用同一版本镜像执行独立迁移任务。 +# 后续优化镜像体积时应拆分 migrator,不能直接删掉迁移命令所需的依赖。 +COPY --from=build --chown=node:node /app /app +USER node +EXPOSE 8800 +CMD ["node", "apps/server/dist/main.js"] diff --git a/deploy/README.md b/deploy/README.md index 0386eb8..93882e3 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,8 @@ # Doodle 云端部署准备 -更新时间:2026-10-20。本文件记录部署方案与待确认条件。已在 1Panel 申请根域及泛域名证书,并安装 PostgreSQL;尚未创建 Doodle 云端应用、应用域名 DNS 记录或镜像仓库,也未改动旧线上云开发链路。 +更新时间:2026-10-22。用户已确认 `doodle_prod` 数据库准备完成;本轮未现场重新核验云端数据,未部署 API 或修改旧线上云开发链路。 + +下一步按 [服务端首次部署操作手册](../docs/v3.5/服务端首次部署操作手册.md) 操作。已补齐 `Dockerfile.server`、`compose.api.yaml` 和两份环境配置模板,采用本机构建 `linux/amd64` 镜像、1Panel 上传导入、复用现有 PostgreSQL 与 OpenResty 的流程。下文的早期计划与状态记录如有差异,以该手册及本段最新状态为准。 ## 已确认条件 diff --git a/deploy/compose.api.yaml b/deploy/compose.api.yaml new file mode 100644 index 0000000..c8ff40a --- /dev/null +++ b/deploy/compose.api.yaml @@ -0,0 +1,45 @@ +name: doodle-prod + +services: + api: + image: ${DOODLE_API_IMAGE:?请在 .env 中填写已导入的镜像标签} + platform: linux/amd64 + container_name: doodle-api-prod + # raw 保留密码中的 $ 等字符,需要 Docker Compose >= 2.30.0。 + env_file: + - path: ./server.env + format: raw + init: true + restart: unless-stopped + stop_grace_period: 30s + ports: + - '127.0.0.1:8800:8800' + networks: + - panel + healthcheck: + # 同时验证 HTTP 和数据库连通性;不代表业务表、微信或七牛已验收。 + test: + - CMD + - node + - -e + - "fetch('http://127.0.0.1:8800/api/v1/health/ready', { signal: AbortSignal.timeout(2500) }).then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + mem_limit: 768m + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + logging: + driver: json-file + options: + max-size: '10m' + max-file: '3' + +networks: + panel: + # 复用 1Panel 已有数据库网络,不创建或接管 PostgreSQL 容器。 + external: true + name: ${PANEL_DOCKER_NETWORK:-1panel-network} diff --git a/deploy/compose.env.example b/deploy/compose.env.example new file mode 100644 index 0000000..a3dad85 --- /dev/null +++ b/deploy/compose.env.example @@ -0,0 +1,3 @@ +# 仅供 Compose 选择镜像和网络;复制为服务器部署目录下的 .env。 +DOODLE_API_IMAGE=doodle-server:20261022-01 +PANEL_DOCKER_NETWORK=1panel-network diff --git a/deploy/server.env.example b/deploy/server.env.example new file mode 100644 index 0000000..01849ee --- /dev/null +++ b/deploy/server.env.example @@ -0,0 +1,29 @@ +# 复制为 server.env,仅在服务器填写真实值,禁止提交仓库或打入镜像。 +# Compose 使用 raw 读取:每行 KEY=value,值不加包裹引号,不写行尾注释。 +NODE_ENV=production +HOST=0.0.0.0 +PORT=8800 +LOG_LEVEL=info +NODE_OPTIONS=--max-old-space-size=384 +API_BODY_LIMIT_BYTES=1048576 +SWAGGER_ENABLED=false +CORS_ORIGINS= +# 初次部署留空;只在确认实际反向代理来源后填写明确 IP/CIDR。 +TRUSTED_PROXY_CIDRS= +JWT_SECRET= +JWT_ISSUER=doodle-store +JWT_AUDIENCE=doodle-mini +JWT_EXPIRES_IN_SECONDS=86400 +WECHAT_MINI_APP_ID= +WECHAT_MINI_APP_SECRET= +# 密码先 URL 编码;主机是数据库容器名,端口是容器内部的 5432。 +DATABASE_URL=postgresql://joey_postgresql:REPLACE_WITH_URL_ENCODED_PASSWORD@1Panel-postgresql-RqP6:5432/doodle_prod?schema=public +WORKSHEET_NEW_WINDOW_DAYS=30 +QINIU_PUBLIC_DOMAIN=https://cdn.joeyone.cn +QINIU_KEY_PREFIX=doodle-assets/ +QINIU_BUCKET=doodle-cn +# 只读联通阶段可一起留空;头像和管理员图片上传验收前必须配置。 +QINIU_AK= +QINIU_SK= +QINIU_REGION= +SITE_REFRESH_HOUR_UTC=22 diff --git a/docs/v3.5/开发交接文档.md b/docs/v3.5/开发交接文档.md index 7115eb8..ab7b955 100644 --- a/docs/v3.5/开发交接文档.md +++ b/docs/v3.5/开发交接文档.md @@ -1,5 +1,7 @@ # Doodle v3.5 开发交接文档 +2026-10-22 更新:用户确认云端 `doodle_prod` 已准备完成。已提供 [服务端首次部署操作手册](./服务端首次部署操作手册.md) 和配套 Docker/Compose/环境模板;本轮仅准备配置和说明,未实际部署云端 API、执行生产迁移或切换小程序地址。当前流程为 Mac 构建 `linux/amd64` 镜像后通过 1Panel 上传导入,复用面板已有 PostgreSQL 与 OpenResty。后文“尚未准备部署配置”“数据库未部署”等属于历史记录,不覆盖本条更新。 + 更新时间:2026-10-19。本次整理当前进度和后续任务,未执行云端部署。验证结果来自此前开发记录,本次文档更新未重新运行全套测试。 2026-10-20 最新决策:云端 PostgreSQL 由 1Panel 安装管理。本地 `doodle_dev` 提供已整理的内容及七牛映射,CloudBase 提供最新用户、收藏、下载记录及题型真实计数;建议在本机独立迁移库整合后导入 `doodle_prod`。此前“仅导内容”和“直接全量复制本地库”的约定均已被替代;逐表来源和补导边界见 [生产数据整合与导入方案](./生产数据整合与导入方案.md)。其余尚未更新的历史部署建议不覆盖本条决策。 diff --git a/docs/v3.5/服务端首次部署操作手册.md b/docs/v3.5/服务端首次部署操作手册.md new file mode 100644 index 0000000..aad49f9 --- /dev/null +++ b/docs/v3.5/服务端首次部署操作手册.md @@ -0,0 +1,401 @@ +# 服务端首次部署操作手册 + +更新时间:2026-10-22。适用腾讯云 Ubuntu 24.04 / x86_64、1Panel 2.3.2、已有 PostgreSQL 与 `doodle_prod` 的环境。 + +用户已确认数据库准备完成;本文不重复建库、初始化表或导入数据,也不表示已现场核验云端数据。本文是操作说明与配套配置,不代表已经部署到云端。 + +## 1. 本次采用的部署方式 + +在 Mac 上构建 Linux 镜像,导出压缩包,通过 1Panel 上传到服务器并导入。服务器只运行构建好的 API,不需要额外安装 Node.js、pnpm、PM2 或源码。暂时无需镜像仓库,后续可改为仓库推送/拉取。 + +```text +小程序 → 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 终端执行服务器命令。 + +```bash +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 容器名和网络模式。也可把实际名称代入: + +```bash +docker inspect --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 执行以下只读查询核对当前目标和迁移历史: + +```sql +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; +``` + +当前代码包含三份迁移: + +```text +0_v35_baseline +20261016000000_category_publication +20261019000000_member_assets +``` + +初始化 SQL 已写入这些迁移记录。不要重新运行初始化 SQL、导入本地 `_prisma_migrations`,也不要删除现有表来绕过迁移错误。 + +## 3. 在 Mac 构建和导出镜像 + +先启动 Docker Desktop。在 **Mac 终端**执行: + +```bash +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`,也不需要提供真实数据库密码、微信或七牛密钥来构建。 + +确认镜像架构并导出: + +```bash +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 终端**执行: + +```bash +mkdir -p /opt/doodle/releases/20261022-01 +mkdir -p /opt/doodle/prod +chmod 700 /opt/doodle/prod +``` + +在 **1Panel → 文件**中: + +1. 将 Mac `/tmp/doodle-release-20261022-01/` 中的压缩包和 `.sha256` 文件上传到服务器 `/opt/doodle/releases/20261022-01/`。 +2. 将仓库 `deploy/` 下的 `compose.api.yaml`、`compose.env.example`、`server.env.example` 上传到 `/opt/doodle/prod/`。 +3. 首次在服务器复制模板:`compose.env.example` → `.env`;`server.env.example` → `server.env`。后续发布不要再次复制模板覆盖真实配置。 + +如果文件页隐藏点文件,打开显示隐藏文件后编辑 `.env`。若通过网页上传大镜像超时,可改用 1Panel 支持的分片上传,或用已有 SSH/SFTP 上传;无需改用浏览器将镜像作为 SQL 文件导入。 + +在 **1Panel 终端**执行: + +```bash +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`: + +```dotenv +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;新后端任务不会关闭旧云任务 | + +数据库连接格式: + +```text +postgresql://joey_postgresql:URL编码后的密码@1Panel-postgresql-RqP6:5432/doodle_prod?schema=public +``` + +这里的 `5432` 是 PostgreSQL **容器内部端口**;数据库主机不是公网 IP,也不是 API 容器的 `127.0.0.1`。不需要开放 PostgreSQL 公网端口。 + +密码中含 `@`、`#`、`/`、`%` 等字符时必须 URL 编码。可在自己的 **Mac 终端**交互转换,输入不会回显,结果只用于配置,不发送到聊天或截图: + +```bash +python3 - <<'PY' +from getpass import getpass +from urllib.parse import quote +print(quote(getpass('数据库原始密码:'), safe='')) +PY +``` + +完成后在 **服务器终端**限制权限并检查编排语法: + +```bash +cd /opt/doodle/prod +chmod 600 .env server.env +docker compose -f compose.api.yaml config --quiet +``` + +检查成功无输出。不要把不带 `--quiet` 的完整配置输出发到聊天,其中可能包含已展开的密钥。此语法检查不能验证密钥、数据库密码或域名是否正确。 + +## 6. 核对迁移状态,再启动 API + +以下在 **服务器 `/opt/doodle/prod`** 执行: + +```bash +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,单独制定基线修复方案 | +| 失败迁移 / 校验和不一致 | 停止,核对初始化脚本与本次镜像版本,不手工把失败记录标为成功 | +| 出现待执行迁移 | 首次预期不应出现;确认这些是计划发布的新增迁移并已备份后,再执行下方迁移命令 | + +需要应用已审查的新迁移时执行: + +```bash +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`。 + +状态确认后启动: + +```bash +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. 先验证服务器内部访问 + +在服务器终端执行: + +```bash +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` 控制;配置七牛密钥后还会处理待清理头像。因此启动前应确认导入的任务、资产状态符合 [生产数据整合与导入方案](./生产数据整合与导入方案.md),不要把开发库的清理队列直接复制到生产。 + +## 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` 忽略证书验证: + +```bash +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. 微信与小程序联调 + +1. 微信后台配置新后端调用微信接口所需的**实际公网出口 IP 白名单**。`10.0.16.14` 是内网地址,不能填它;若出现微信 `40164`,核对实际出口与白名单。 +2. 服务器应能出站访问微信接口及七牛服务。检查 AppID/AppSecret 配对,不能用浏览器随便编造一个 code 判断登录是否成功。 +3. 配置 request、uploadFile 合法域名为 `https://doodle-api.joeyone.cn`;资源下载使用的 downloadFile 合法域名包含 `https://cdn.joeyone.cn`。依据实际接口用途添加,不在合法域名中填写 `/api/v1` 路径。 +4. API 公网验收成功后,再修改 `doodle-mini/miniprogram/config/api.ts` 中的 `productionBaseUrl` 为 `https://doodle-api.joeyone.cn/api/v1`。`localBaseUrl` 保留本地;未部署 staging 时不将其描述为独立可用的测试环境。本手册不会自动修改小程序地址。 +5. 先在 develop 的 Debug 页切到 production,再用体验版验证真实登录、分类、题型、首页、收藏、下载记录、资料保存、头像上传与管理员发布。trial/release 固定 production,不能通过 Debug 切到本地。 +6. 使用七牛真实密钥做上传验收前,确认 CloudBase 用户头像绑定、七牛对象 key 和 `assets` 权限映射符合预期,避免把本地测试用户当成线上用户。 + +只有 HTTP、数据库、真实微信登录和必要业务都验收后,才安排新的正式版发布。旧线上 CloudBase 在并行期仍产生新数据;部署本身不会实现双向同步,正式切换前还需按迁移方案安排最新补导和对账。 + +## 10. 日常维护、更新和回退 + +### 查看状态与重新加载配置 + +在服务器 `/opt/doodle/prod`: + +```bash +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` 不会读取新环境变量: + +```bash +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` 处理的是进程退出。 + +### 发布新版本 + +1. 在本机使用新标签重新构建,保存 Git 提交/工作区说明、构建输出和镜像 ID。 +2. 导出、校验、上传、`docker load`;保留旧镜像与旧配置副本。配置副本含密钥,仅存权限受限目录,不能打入发布包。 +3. 完整备份数据库并确认可下载恢复;查看本次迁移 SQL,判断旧程序能否兼容新结构。 +4. 编辑 `.env` 的 `DOODLE_API_IMAGE` 为新标签,先运行 `db:status`;需要时执行 `db:deploy`,再 `up -d --pull never --wait --wait-timeout 120`。 +5. 重做内部和公网健康、业务检查,以及涉及本次改动的小程序验收。 + +当前是单 API 容器更新,重建期间会有短暂中断;若以后需要零中断,再设计双实例切换。本手册不宣称零停机。 + +### 应用回退 + +若数据库结构仍兼容旧镜像,把 `.env` 标签改回上一版本,恢复匹配的运行配置,再执行: + +```bash +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 或修改小程序接口地址。 + +## 13. 官方参考 + +- [Docker 跨平台镜像构建参数](https://docs.docker.com/reference/cli/docker/buildx/build/) +- [Docker 镜像导出与压缩](https://docs.docker.com/reference/cli/docker/image/save/) +- [Docker Compose 服务配置:env_file、healthcheck、ports 等](https://docs.docker.com/reference/compose-file/services/) +- [1Panel 创建反向代理网站](https://docs.fit2cloud.com/1panel/user_manual/websites/website-create/) +- [1Panel 网站 HTTPS 与反向代理配置](https://docs.fit2cloud.com/1panel/user_manual/websites/website-config-basic/)