【AI全栈】前端转全栈需了解的 Docker + CI/CD 核心知识
📌 系列简介:《my-resume AI实战》部署过渡篇。
本篇:建立最小部署认知 / 下一篇:真实踩坑复盘
前端转 JS 全栈,正在学部署,理解难免有偏差,欢迎批评指正 ~
(2026-05 更新:部署方案已从手动 SSH 升级为一键
release-from-local.sh,镜像构建也升级为多阶段 + Next standalone 输出。)
写在前面
两年前掘金写了篇博客 学习TailWindCSS顺便打造个性化在线简历项目 当时也有朋友问怎么没做CICD ~
当时的心路历程,1是麻烦,2是不会… https nginx 把这些折腾上去都够呛,不想学新的了 ……
AI时代,再停留真要被落下了,那么就让AI带着自己一起跑吧。虽上高速但只跑 60Km/h = =
废话少说。接下来把学习和折腾历程发出来。这篇想把我理清楚这三个边界的过程写下来—— 三个边界:构建、运行、数据。 不是完整的 Docker 教程,是个人前端视角转全栈阶段,用得上的那些。
一、先把概念对齐:最容易混的 6 组
学 Docker 最痛苦的阶段,是概念还没对齐就开始操作—— 报错了不知道是哪一层的问题,改了半天改错了地方。
我整理了最容易混的 6 组,先对齐再往下走:
| 容易混淆 | 正确认知 |
|---|---|
| Image vs Container | Image 是封箱好的运行包;Container 是它跑起来的实例。镜像像类,容器像对象。 |
| Registry / Repository / Tag | Registry 是仓库服务(比如 GHCR);Repository 是镜像名空间;Tag 是具体版本指针。 |
| ARG vs ENV | ARG 只在构建期有效;ENV 会进入运行时容器,服务启动后还在。 |
| EXPOSE vs ports | EXPOSE 只是声明,不对外开放;真正映射端口靠 Compose 的 ports 字段。 |
| Volume vs Bind Mount | Volume 更适合生产持久化;Bind Mount 更适合本地开发联调。 |
| Build vs Run | 构建阶段解决”产物一致性”;运行阶段解决”实例可用性与配置注入”。 |
把这 6 组搞清楚,排障时至少知道问题出在哪一层。
二、为什么”上线后就能运行”?
搞清楚概念之后,我才真正理解了一件事——
上线的不是源码,是可运行工件。
在 Docker 路线里,这个工件是镜像(Image):包含代码、运行环境(Node、依赖、系统库)、启动命令。
服务器只做两件事:拉镜像 + 启容器。
这就是”发布成功即可运行”的基础。 也是为什么换了服务器、换了机器,只要能拉到镜像,服务就能跑起来。
三、从代码到用户访问:一条链路
这条链路的关键是:构建和运行分离。
- 构建在本地 / CI 完成:有算力、有缓存、可重试,产物一致
- 运行在 ECS 完成:尽量轻量,只负责拉起服务
我现在用的是一键脚本 deploy/ecs/release-from-local.sh——打上 git tag 之后,一条命令完成构建镜像、推送到 GHCR、SSH 到 ECS 拉取并启动:
./deploy/ecs/release-from-local.sh \ --tag v2.3.1 \ --ecs-host your-ecs-ip \ --stack-env ./.env.stack.local我之前习惯”SSH 上服务器直接跑”,后来发现这样有两个问题: 本地环境和服务器环境不一致,出了问题很难复现; 而且没有版本记录,出事了不知道回滚到哪里。
Docker 镜像 + GHCR + tag 版本号,解决的就是这两个问题。
四、Dockerfile:真正需要懂的 7 件事(含真实案例)
Dockerfile 定义镜像怎么构建。经过多轮迭代,我项目里的 Dockerfile 已经演进为多阶段构建。以下是 apps/server/Dockerfile 的实际结构(5 阶段):
# 阶段 1:基础镜像(Node + pnpm)FROM node:22-slim AS baseRUN npm install -g pnpm@10.8.0WORKDIR /app
# 阶段 2:运行时基础(系统依赖,如 PDF 导出用的中文字体)FROM base AS runtime-baseRUN apt-get update && apt-get install -y fonts-noto-cjk
# 阶段 3:依赖安装(利用缓存 —— 只复制 package.json)FROM base AS depsCOPY pnpm-lock.yaml pnpm-workspace.yaml ./COPY apps/server/package.json apps/server/package.jsonCOPY packages/*/package.json packages/*/package.jsonRUN pnpm install --frozen-lockfile --filter @my-resume/server...
# 阶段 4:构建(编译 TypeScript → dist)FROM deps AS builderCOPY . .RUN pnpm --filter @my-resume/server buildRUN pnpm --filter @my-resume/server deploy --legacy --prod /prod/server
# 阶段 5:运行(只复制必要产物,镜像最小化)FROM runtime-base AS runnerENV NODE_ENV=productionCOPY --from=builder /prod/server/node_modules ./node_modulesCOPY --from=builder /app/apps/server/dist ./apps/server/distCMD ["node", "apps/server/dist/src/main.js"]Web 和 Admin 端用的是 Next.js standalone 输出,4 阶段:
FROM node:22-slim AS base # 基础环境# ...FROM base AS deps # 依赖安装# ...FROM deps AS builder # Next.js build (输出 .next/standalone)ARG NEXT_PUBLIC_API_BASE_URL # 构建时注入 API 地址RUN pnpm --filter @my-resume/web build
FROM base AS runner # 运行层:只复制 standalone 产物COPY --from=builder /app/apps/web/.next/standalone ./COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/staticCMD ["node", "apps/web/server.js"]缓存层的顺序很重要
Dockerfile 每条指令都是一层,前面的层变了,后面的缓存全部失效。
我踩过的坑:把 COPY . . 放在 RUN pnpm install 前面——
每次改一行业务代码,依赖就重新安装一遍,构建时间从 30 秒变成 3 分钟。
正确做法:先复制 package.json,装完依赖,再复制业务代码。
依赖没变,那一层缓存就命中,只有业务代码那层重新构建。
多阶段构建:让镜像更小
Builder 阶段的编译缓存、devDependencies,全部不进最终镜像。 对小规格 ECS 来说,镜像越小,拉取越快,发布越稳。 我的 server 镜像从 1.2GB 压到了约 380MB,web/admin 用 Next standalone 后约 250MB,就靠这一步。
五、Compose:多服务的”启动说明书”
my-resume 有三个服务:web(用户端,5555 端口)、admin(后台,5566 端口)、server(API,5577 端口)。
Compose 让这三个服务按规则一起跑。
真实用到的 docker-compose.yml 核心字段:
services: server: build: # 或 image: ghcr.io/... 镜像模式 context: . dockerfile: apps/server/Dockerfile env_file: - .env # 运行时配置(JWT、数据库、AI Provider 等) ports: - '5577:5577' volumes: - resume_sqlite_data:/app/.data # SQLite 数据库持久化
web: build: dockerfile: apps/web/Dockerfile args: NEXT_PUBLIC_API_BASE_URL: http://localhost:5577 # 构建时注入 RESUME_API_BASE_URL: http://server:5577 # SSR 内部调用 depends_on: - server # 启动顺序(只保证顺序,不等就绪) ports: - '5555:5555'
admin: build: dockerfile: apps/admin/Dockerfile args: NEXT_PUBLIC_API_BASE_URL: http://localhost:5577 depends_on: - server ports: - '5566:5566'
volumes: resume_sqlite_data: # 命名卷:数据不随容器销毁而丢失有一个我之前理解错的地方:
depends_on 只保证容器启动顺序,不保证服务真的可用。
数据库容器启动了,不代表数据库已经接受连接了。 要配合应用层重试,才能真正保证依赖可用。
六、线上稳定性的最小闭环
跑通部署之后,我意识到还差三件事:知道服务活没活、出问题能看日志、出问题能回滚。
健康检查
GET /api/health 端点不仅检查进程存活,还检查数据库连通性。
我遇到过进程在但数据库连接断了,外面看着”绿色”,实际上所有请求都在报错。
日志
出了问题,先看容器日志:
docker compose logs -f server大多数问题——环境变量缺失、数据库连接失败、端口冲突——日志里都有。 先看日志,再看源码,别一上来就改代码。
回滚
镜像 tag 要用可追溯的版本号,不要只用 latest。
ECS 上的发布管理采用了快照目录 + 软链接策略:
~/my-resume/ current -> release-snapshots/v2.3.0 ← 当前运行版本 release-snapshots/ v2.2.9/ ← 上上个版本 v2.3.0/ ← 上一个版本(保留) v2.3.1/ ← 新版本发布时在 release-snapshots/ 下创建新目录 → 拉起新容器 → 验证通过后切换 current 软链接。出问题时,回滚就是一条命令:
./deploy/ecs/rollback.sh # 自动切回上一个 release snapshot不需要重新排查编译环境,不需要重新部署代码——切版本,就完了。
七、CI/CD 在干嘛
可以把它理解成三段流水线:
1. CI 检查 代码合格(test、typecheck、build 验证) ↓2. Build 构建镜像,push 到 GHCR,打上版本 tag ↓3. CD 部署 ECS 拉指定 tag,创建 release snapshot,compose up比”SSH 上服务器手动构建”稳的原因:
- 构建环境一致,不会出现”我本地好的”
- 每个版本有 tag(如
v2.3.1),可追溯 - ECS 保留最近两个 release snapshot,出问题可快速回滚
- 支持增量发布:只改
web/admin时,server 镜像从上一个 tag 复用
八、常用命令:够用就行
查看状态
docker ps # 看运行中的容器docker images # 看本地镜像docker compose ps # 看 compose 服务状态docker compose logs -f # 实时看日志部署操作
docker compose pull # 拉最新镜像docker compose up -d # 启动(镜像模式不重新构建)docker compose down # 停止并删除容器一键发布(本地 → GHCR → ECS)
# 完整流程:构建镜像 + 推送 + SSH 到 ECS 部署./deploy/ecs/release-from-local.sh --tag v2.3.1 --ecs-host <ip>
# 只推送构建产物,不部署(用于 GitHub Actions CI)./deploy/ecs/release-from-local.sh --tag v2.3.1 --skip-deploy排障
docker inspect <container> # 看容器详细配置docker exec -it <container> sh # 进容器排查curl -i http://127.0.0.1:<port> # 测试服务是否可达九、这阶段该学多深?
我的判断:先学”能跑通 + 能排障 + 能回滚”,不要一次学太深。
现在要会的:
- 写/读 Dockerfile,理解多阶段构建和缓存层顺序
- 读懂 Compose 核心字段(
image/build/ports/env_file/volumes/depends_on) - 看日志、看健康检查
- 跑通本地构建 + ECS 拉镜像部署的完整闭环
- 用 tag 做回滚,理解 snapshot 管理
可以后置的:
- 内核细节(cgroups / namespace / overlayfs)
- 大规模集群调度(K8s)
- 复杂网络与安全策略
不是这些不重要,是当前阶段投入产出比不高。 把能跑通、能排障、能回滚做扎实,比看完整本书更有用。
写在最后
做完这套部署之后,我对”部署”这件事的理解变了——
它不是神秘的运维操作,是工程化的延伸: 把可运行工件,稳定地、可追溯地、可回滚地送到线上。
从最开始的手动 SSH + npm run build,到 Docker 一键构建,再到 release-from-local.sh 一条命令完成全流程——
每一步都不是一次性做对的,而是在真实踩坑中慢慢补起来的。
前端做工程化,讲的是构建产物的一致性和可维护性。 Docker + CI/CD 做的是同一件事,只是把边界从”浏览器能跑”扩展到了”服务器能跑”。
想清楚这一点之后,Docker 就没那么陌生了。
参考文档
- Docker Overview
- Dockerfile reference
- Multi-stage builds
- Compose file reference
- Next.js Standalone Output
- Compose startup order
- Volumes
- Twelve-Factor App / Config
- my-resume 部署文档
*昇哥 · 2026年6月 my-resume 部署上线途中,把想清楚的事写下来
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!