【AI全栈】前端转全栈需了解的 Docker + CI/CD 核心知识

2712 字
14 分钟
【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 ContainerImage 是封箱好的运行包;Container 是它跑起来的实例。镜像像类,容器像对象。
Registry / Repository / TagRegistry 是仓库服务(比如 GHCR);Repository 是镜像名空间;Tag 是具体版本指针。
ARG vs ENVARG 只在构建期有效;ENV 会进入运行时容器,服务启动后还在。
EXPOSE vs portsEXPOSE 只是声明,不对外开放;真正映射端口靠 Compose 的 ports 字段。
Volume vs Bind MountVolume 更适合生产持久化;Bind Mount 更适合本地开发联调。
Build vs Run构建阶段解决”产物一致性”;运行阶段解决”实例可用性与配置注入”。

把这 6 组搞清楚,排障时至少知道问题出在哪一层。


二、为什么”上线后就能运行”?#

搞清楚概念之后,我才真正理解了一件事——

上线的不是源码,是可运行工件。

在 Docker 路线里,这个工件是镜像(Image):包含代码、运行环境(Node、依赖、系统库)、启动命令。

服务器只做两件事:拉镜像 + 启容器

这就是”发布成功即可运行”的基础。 也是为什么换了服务器、换了机器,只要能拉到镜像,服务就能跑起来。


三、从代码到用户访问:一条链路#

flowchart LR A["本地开发"] --> B["git tag + push"] B --> C["release-from-local.sh<br/>一键构建 + 推送"] C --> D["docker buildx<br/>多服务并行构建"] D --> E["推送到 GHCR<br/>带 v2.3.1 tag"] E --> F["ECS release.sh<br/>pull 镜像 + compose up"] F --> G["Nginx 反向代理"] G --> H["用户访问域名"]

这条链路的关键是:构建和运行分离。

  • 构建在本地 / CI 完成:有算力、有缓存、可重试,产物一致
  • 运行在 ECS 完成:尽量轻量,只负责拉起服务

我现在用的是一键脚本 deploy/ecs/release-from-local.sh——打上 git tag 之后,一条命令完成构建镜像、推送到 GHCR、SSH 到 ECS 拉取并启动:

Terminal window
./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 base
RUN npm install -g pnpm@10.8.0
WORKDIR /app
# 阶段 2:运行时基础(系统依赖,如 PDF 导出用的中文字体)
FROM base AS runtime-base
RUN apt-get update && apt-get install -y fonts-noto-cjk
# 阶段 3:依赖安装(利用缓存 —— 只复制 package.json)
FROM base AS deps
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/server/package.json apps/server/package.json
COPY packages/*/package.json packages/*/package.json
RUN pnpm install --frozen-lockfile --filter @my-resume/server...
# 阶段 4:构建(编译 TypeScript → dist)
FROM deps AS builder
COPY . .
RUN pnpm --filter @my-resume/server build
RUN pnpm --filter @my-resume/server deploy --legacy --prod /prod/server
# 阶段 5:运行(只复制必要产物,镜像最小化)
FROM runtime-base AS runner
ENV NODE_ENV=production
COPY --from=builder /prod/server/node_modules ./node_modules
COPY --from=builder /app/apps/server/dist ./apps/server/dist
CMD ["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/static
CMD ["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 端点不仅检查进程存活,还检查数据库连通性。 我遇到过进程在但数据库连接断了,外面看着”绿色”,实际上所有请求都在报错。

日志#

出了问题,先看容器日志:

Terminal window
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 软链接。出问题时,回滚就是一条命令:

Terminal window
./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 复用

八、常用命令:够用就行#

查看状态#

Terminal window
docker ps # 看运行中的容器
docker images # 看本地镜像
docker compose ps # 看 compose 服务状态
docker compose logs -f # 实时看日志

部署操作#

Terminal window
docker compose pull # 拉最新镜像
docker compose up -d # 启动(镜像模式不重新构建)
docker compose down # 停止并删除容器

一键发布(本地 → GHCR → ECS)#

Terminal window
# 完整流程:构建镜像 + 推送 + 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

排障#

Terminal window
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 就没那么陌生了。


参考文档#


*昇哥 · 2026年6月 my-resume 部署上线途中,把想清楚的事写下来

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【AI全栈】前端转全栈需了解的 Docker + CI/CD 核心知识
https://blog.fridolph.top/posts/2026-04-27__pre-prj-2/
作者
Fridolph
发布于
2026-04-27
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录