TTeamDesk 学习手册
本章目录 展开导航 ↓

29 · 独立容器部署:把代码、进程和持久数据分开理解#

小周拿到一个检查通过的版本,想在本机验收。Web 首页打开了,导出却一直等待;他又以为重新构建镜像会把数据库回到初始状态。要解释这两个误会,先分清镜像、容器、网络和卷的生命周期。

本章使用单独 Compose 项目 teamdesk-stage6 和 .env.stage6 作为学习部署,不复用日常 .env.deploy。以下是供你执行的验收步骤,不会因阅读教程自动部署,也不使用有真实业务数据的环境。

1. 从 Dockerfile 找三个启动入口#

打开 Dockerfile,先找各 FROM 的 AS 名称,再读每个目标最后的 CMD:

目标 从哪里取得内容 入口
web build 阶段的 standalone、static、public node server.js
worker worker-deps 的生产依赖 + build 的 src、scripts 等 tsx src/worker/index.ts
migrate deps 的 Prisma CLI、schema 和迁移文件 prisma migrate deploy

共同的 runtime 以 Node 24 Debian slim 为基础并安装 OpenSSL / CA;base 固定安装 pnpm。deps 用锁文件安装完整构建依赖,build 再复制源码并执行 pnpm build。不要把“最终 Worker 只装生产依赖”误读成“构建阶段不需要开发依赖”。

package.json 的 build 先 prisma generate,再 next build。生成 Client 位于 src 内,因此 Worker 从 build 复制 src 时也拿到它。主机上的 src/generated/prisma 被 .dockerignore 排除,并不表示镜像运行时可以缺这份生成代码,而是应由镜像内构建生成。

2. standalone 为什么还要复制两类资源#

next.config.ts 配置 output='standalone'。已对照本项目安装版本附带的 output 文档:Next 追踪 Web 所需文件并产出最小 server.js,但 public 和 .next/static 默认不一起复制。

dockerfile
# 源码节选:Web 最终镜像
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public

缺第二行可能让页面 HTML 能打开却缺脚本 / 样式。standalone 是 Web 运行产物,不会自动启动独立 Worker,也不会把教程站变成业务路由。tutorials/site/dist/ 仍由静态服务单独提供,当前 Dockerfile 不打包教程阅读站。

最终应用以 node 用户运行;Worker 使用 tsx,所以 tsx 在 dependencies 中,不能随意移到仅开发依赖而不改镜像策略。迁移镜像保留 CLI,与只提供 Web 请求的运行目标承担不同职责。

3. 环境配置在哪个时刻进入容器#

.dockerignore 排除主机 node_modules、.next、Git、真实 .env* 等;Dockerfile 也没有复制真实配置或用构建参数传密钥。构建安装依赖与生成代码,不要求把生产数据库凭据塞进镜像层。

在项目根目录首次准备本章专用配置:

bash
# 已有 .env.stage6 时不覆盖,检查并补齐它
test -e .env.stage6 || cp .env.deploy.example .env.stage6
# 分别执行三次,每次产生一个独立值,填入下述三个字段
openssl rand -hex 32

将三个独立值分别填写 POSTGRES_PASSWORD、REDIS_PASSWORD、BETTER_AUTH_SECRET。保留本地验收的 BETTER_AUTH_URL=http://localhost:3100、WEB_PORT=3100,可将 ↗ QUEUE_PREFIX 改为 teamdesk-stage6。用十六进制避免密码拼接 URL 的编码歧义,真实文件不要提交。

若 3100 已被占用,先为自己的学习环境选择空端口,并同时修改 WEB_PORT、BETTER_AUTH_URL 和后续验收 URL,不停止别人的服务。认证 Origin 与浏览器访问地址也必须相符。

Compose 的 --env-file 用于变量插值,compose.deploy.yaml 的 environment 再明确传给各服务;不是任意写进 env 文件的字段都会自动进入所有容器。不要为核对配置把展开后的完整连接串贴到终端记录或截图中。

4. 启动依赖表达什么条件#

text
PostgreSQL 健康 → migrate 以 0 退出 → Web 启动
                                      ↓
Redis 健康 ──────────────────────→ Worker 启动

两类业务进程都依赖 migrate 成功,Worker 另依赖 Redis 健康。Web 不等 Redis 是为了 Redis 故障时基本工单仍可使用;这不代表导出能在没有任务依赖时立即完成。

在本章专用环境执行:

bash
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml up --build --wait --wait-timeout 240
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml ps -a
curl --fail http://localhost:3100/api/health

预期 migrate 是成功退出的一次性任务,Web / Worker 是持续运行服务;不要把 migrate 不在 Up 列表就认定部署失败。--wait 等配置的就绪条件,240 秒是等待预算,不是业务完成 SLA。未通过时先看失败服务及日志,不绕过迁移启动新代码。

5. 容器地址为什么不能复制宿主机连接#

发起连接的位置 目标地址 原因
浏览器 / 本机 curl localhost:3100 Web 映射宿主机端口
Web / Worker 容器 postgres:5432、redis:6379 同 Compose 网络的服务 DNS
Web 容器自身探针 127.0.0.1:3000 检查容器内 Web 监听端口

容器里的 localhost 是它自己,不是主机,也不是另一个服务。部署的数据库、Redis 没有发布宿主机端口,因此不要拿开发用 localhost:5433 直接检查这套库;必要操作经对应服务 exec 进入。

Web 内部 HOSTNAME=0.0.0.0,但对宿主机仅绑定 127.0.0.1。前者允许容器网络接入,后者限制宿主机对外监听,它们不矛盾。正式域名、HTTPS 与反向代理仍需另外配置。

-p teamdesk-stage6 覆盖文件默认项目名,给这套网络和命名卷独立命名;前提是这个名字原先没有被其他部署使用。新项目名不复制开发数据,账号列表为空可能只是连到新库,不是数据被删除。

6. 镜像升级、停止与回滚分别改变什么#

镜像装代码和运行文件,容器提供进程,PostgreSQL / Redis 的命名卷保存持久状态。stop 停进程,重新 build 换代码,两者都不会自动把数据恢复成昨天。

学习环境里模拟升级,也应先记下当前版本、确认数据可恢复,再停止业务服务让迁移先于新业务恢复:

bash
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml stop web worker
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml up --build --wait --wait-timeout 240
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml logs --tail 100 migrate

检查迁移退出结果及待验收版本,随后重做健康与业务 smoke。这是有停机窗口的简单升级,不是零停机滚动发布,也没有自动发布旧镜像的回滚按钮。

当前仓库迁移向前应用 SQL;旧代码能否读新结构要单独判断。若需要数据库恢复,必须事先有可验证的备份与恢复方案;仓库尚无自动备份恢复脚本,本章不会把未演练过的恢复写成已保障。

还有一个容易忽略的故障:修改 POSTGRES_PASSWORD 并重启已有数据库卷,并不等于为已有 PostgreSQL 用户自动改好密码。初始化配置与已持久化账号是两回事,应走明确的凭据变更流程,不能靠删除数据卷修复连接。

7. 三种部署故障的排查路径#

迁移失败,Web 没启动。 查 migrate 日志、退出码、对应 schema / migrations 及数据库连接。先修复升级问题,不去取消 service_completed_successfully 条件。

首页正常,静态请求 404 或导出等待。 前者查 Web 镜像静态资源复制和请求路径;后者查 Worker 容器、Redis、账本与队列前缀。相同的“页面不正常”可能位于不同进程。

新镜像启动后还看到旧任务。 先确认卷与数据库连接。持久账本保留本来就是恢复任务的依据;不要期待构建顺便清库,也不要为得到干净截图删除已有数据。

拓展阅读非 root 能解决哪些问题?

降低应用默认权限有价值,但不会替你建立 HTTPS、备份、密钥轮换或数据库最小权限。评估部署时要按真实边界逐项验证,不能把 USER node 看成整套运行保障。

8. 动手验证与练习#

本章只把隔离部署启动到可观察状态。记录项目名、代码提交、Web 端口、migrate 结果、Web / Worker 状态;按第 30 章读探针和退出日志,再按第 31 章验证实际数据链路。日常暂停本章环境可用同一组参数执行 stop;本章不需要清卷。

练习 A:Worker 从生产依赖安装,生成 Client 从哪里来#

动手练习查看参考答案

参考答案:构建阶段的 pnpm build 先生成 Client;Worker 最终镜像从 build 复制 src,包含新生成的 Client。主机旧生成物被排除,不能依赖它碰巧存在。两份来源与复制路径都要读清楚。

练习 B:为什么不让每个 Web / Worker 启动时各跑一次 migrate#

动手练习查看参考答案

参考答案:重启与扩容会使多个进程同时承担结构升级,职责与故障边界混乱。当前独立 migrate 成功后再启动业务,更容易判断表结构是否准备好;它仍不是跨版本兼容与备份的替代。

练习 C:回退代码后字段不兼容,重新 build 能恢复数据吗#

动手练习查看参考答案

参考答案:不能。镜像不包含数据库历史快照,构建不撤销迁移。先判断能否向前修复或让旧代码兼容新结构;需要恢复数据时使用事先验证的恢复路径,不能把清卷当回滚。

链路:锁文件构建与 Client 生成 → 三个运行目标 → 运行时注入配置 → 数据库就绪与迁移 → Web / Worker 启动 → 持久卷承接数据 → 健康和业务验证决定是否可用。

把知识变成自己的理解

完成本章练习,再继续下一站。

TEAMDESK HANDBOOK · 从一张工单,走进全栈开发

搜索教程

输入关键词,搜索全部章节。

阅读设置

外观
正文字号

从一张工单出发,读懂数据如何流转。

设置与进度仅保存在当前浏览器。