CI/CD 实战指南 - 前后端分离部署 / Practical CI/CD for Separately Deployed Frontends and Backends
本文档记录了关于后端 CI/CD 自动化部署的技术讨论,包含原理、方案对比和实战配置
目录
- 一、CI/CD 基本原理
- 二、解决方案对比
- 三、GitHub Actions 实战配置
- 四、Docker vs 直接操作源码
- 五、零停机部署方案
- 六、前后端分离部署架构
- 七、无状态 vs 有状态
- 八、JWT vs Session
一、CI/CD 基本原理
核心概念
| 概念 | 全称 | 含义 |
|---|---|---|
| CI | Continuous Integration | 持续集成 |
| CD | Continuous Delivery | 持续交付 |
| CD | Continuous Deployment | 持续部署 |
工作流程(观察者模式)
触发(Trigger) → 监听(Listen) → 执行(Execute)- 触发:向代码仓库(GitHub/Gitee)提交代码
- 监听:CI/CD 工具通过 Webhook 监听到仓库变动
- 执行:工具自动运行预设脚本(拉取代码、编译、运行测试、打包、部署)
二、解决方案对比
方案对比表
| 方案 | 成本 | 难度 | 适用场景 |
|---|---|---|---|
| GitHub Actions | 免费 | ⭐⭐ | 代码托管在 GitHub |
| 腾讯云 Coding | 免费 | ⭐⭐ | 使用腾讯云服务器 |
| Gitee Go | 免费 | ⭐⭐ | 代码托管在 Gitee |
| 宝塔面板 Webhook | 免费 | ⭐ | 服务器有宝塔面板 |
| 自建 Webhook | 免费 | ⭐⭐⭐ | 定制化需求 |
推荐方案
- 轻量化首选:GitHub Actions(免费、不用维护)
- 国内厂商:腾讯云 Coding / 蓝鲸
- 最简方案:宝塔面板 WebHook 插件
三、GitHub Actions 实战配置
目录触发配置
项目结构:
/ (root)
├── frontend/
├── backend/ <-- 触发部署的目标
└ contract.ts通过 paths 配置限定触发条件:
on:
push:
branches: [ main ]
paths:
- 'backend/**' # 只有后端代码变动才触发
- 'contract.ts' # 共有协议变动也触发完整部署配置示例
name: Deploy Node.js Backend
on:
push:
branches: [ main ]
paths:
- 'backend/**'
- 'contract.ts'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to Tencent Cloud
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: root
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /www/wwwroot/your-project/backend
git pull origin main
npm install --production
pm2 restart allGitHub Secrets 配置
| Secret 名称 | 说明 |
|---|---|
| SERVER_IP | 腾讯云公网 IP |
| SERVER_SSH_KEY | SSH 私钥 |
配置路径:Settings → Secrets and variables → Actions → New repository secret
四、Docker vs 直接操作源码
对比表
| 特性 | 直接操作源码 | Docker 镜像 |
|---|---|---|
| 环境依赖 | 依赖服务器本地 Node/NPM | 镜像内置,服务器只需 Docker |
| 部署失败 | 可能残留半截损坏的代码 | 启动失败不影响旧镜像,可秒级回滚 |
| 扩容性 | 想加服务器需从头配环境 | 只需拉取镜像,10秒开10个节点 |
| 安全性 | SSH 脚本拥有高级修改权限 | 脚本只拥有"操作容器"有限权限 |
| 构建位置 | 在生产服务器上构建 | 在云端(GitHub Actions)构建 |
Docker 优势总结
- 环境一致性:无论在哪运行,镜像内部环境像素级一致
- 部署原子化:部署动作变成"下载 → 替换",速度快且稳
- 进程管理简化:不需要手动进入目录,Docker 自动重启
- 易于迁移扩展:换服务器只需跑镜像,秒级恢复
基础 Dockerfile 示例
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "dist/index.js"]五、零停机部署方案
方案对比
| 方案 | 复杂度 | 停机时间 | 适用场景 |
|---|---|---|---|
| PM2 reload | ⭐ | 极短 | 不用 Docker 时的首选 |
| Docker + Nginx | ⭐⭐ | 0秒 | 工业级标准做法 |
| Blue-Green | ⭐⭐⭐ | 0秒 | 高可用要求 |
方案 A:PM2 平滑重启
pm2 reload # 先启动新进程,等准备好再杀旧进程原理:它会先启动一个新进程,等新进程准备好了,再把旧进程杀掉。
方案 B:Docker + Nginx 滚动更新
1. 旧容器(V1)跑在8001端口,Nginx指向8001
2. 启动新容器(V2)在8002端口
3. 健康检查:等待8002返回200
4. Nginx配置改为指向8002,nginx -s reload
5. 确认V2稳定后删除V1容器方案 C:蓝绿部署
- 维护两套完全一样的环境(蓝和绿)
- 当前流量在"蓝"
- 在"绿"里部署新代码并测试
- 瞬间把路由器拨向"绿"
六、前后端分离部署架构
部署对比
| 特性 | 前端 (Vercel) | 后端 (Node.js + MySQL) |
|---|---|---|
| 产物形式 | HTML/JS/CSS (静态资源) | 运行中的进程 (Node 实例) |
| 环境依赖 | 只要浏览器能跑 | 依赖特定 Node 版本、系统库 |
| 状态存储 | 无状态 | 有状态 |
| 扩展方式 | CDN 自动分发 | 需增加实例、配置负载均衡 |
| 推荐方案 | Vercel 托管 | Docker 容器化 |
黄金架构
前端 (Vercel):负责"美",极速响应用户请求
↓ API
后端 (Docker on 腾讯云):负责"重",复杂业务逻辑
↓
数据库 (MySQL):数据持久化
↑
GitHub Actions:总指挥,代码一推,两边同时更新七、无状态 vs 有状态
核心概念
| 概念 | 比喻 | 特点 |
|---|---|---|
| 无状态 (Stateless) | 快餐店员 | 不记用户信息,换一个店员也能继续工作 |
| 有状态 (Stateful) | 档案管理员 | 必须记住数据,数据丢失会影响业务 |
无状态后端 API 特点
- 可随便销毁、重建、横向扩展
- 不在服务器内存存关键数据
- 用户信息存外部(Redis 或数据库)
有状态数据库特点
- 数据必须持久化
- 不可随容器销毁
- 需要挂载卷(Volume)
Docker 中的实现
无状态后端:
docker rm 旧容器 && docker run 新镜像 # 不心疼,直接删有状态 MySQL:
docker run -v /my/data/mysql:/var/lib/mysql ...把数据"投影"到服务器硬盘,数据就永久保存了
八、JWT vs Session
对比表
| 特性 | JWT (Token) | Session (Redis) |
|---|---|---|
| 存储位置 | 客户端(浏览器/APP) | 服务端(Redis/数据库) |
| 数据同步 | 不需要同步 | 需要同步 |
| 撤销登录 | 难(除非到期或拉黑) | 易(直接删 Session ID) |
| 服务器负担 | 费 CPU(计算签名) | 费内存/网络(读 Redis) |
Session 存续方式
| 方式 | Docker 兼容 | 说明 |
|---|---|---|
| 存内存 | ❌ 死穴 | Docker 1号存的Session,Docker 2号看不到 |
| 存数据库 | ⚠️ 可行但慢 | 每次请求都要查数据库 |
| 存 Redis | ✅ 工业级标配 | 读写极快,支持 Docker 扩容 |
JWT 优势
- 所有信息加密在 Token 字符串里
- 所有 Docker 容器用同一个 JWT_SECRET 就能解密
- 不需要查数据库就知道用户是谁
- 天生为 Docker 而生
避坑指南
❌ 错误做法:
- 把 Session 存内存
- 把上传文件存到
backend/uploads本地文件夹
✅ 正确做法:
- 用 JWT 或 Redis 存 Session
- 文件存到云存储(COS)或共享云硬盘
完整对话记录
对话 1:CI/CD 原理和方案
用户提问:我们现在来进行一些技术栈上的讨论,就是我现在有一个前后端分离的任务,然后前段时部署在vercel上面,但是由于我无法使用Render这种cicd部署后端,所以我是买了腾讯云的服务器然后在服务器里面git clone我的后端代码进行运行,这就导致我的后端每次更新代码都需要重新进入服务器手动运行一下,所以我想知道有没有cicd工具啊??就是在我的服务器里面配置一下??或者说国内有没有后端的cicd厂家??然后我们可以从cicd的原理开始说起
AI回答:典型的开发者成长阶段:从"手动挡"转向"自动挡"。频繁登录服务器进行 git pull 和重启服务不仅低效,而且容易出错。让我们从原理聊起,看看如何让你的腾讯云服务器实现自动化部署。
CI/CD 基本原理:CI/CD 其实是三个概念的缩写:持续集成 (Continuous Integration)、持续交付 (Continuous Delivery) 和 持续部署 (Continuous Deployment)。其核心逻辑可以简化为一个**"观察者模式"**:
- 触发 (Trigger):你向代码仓库(GitHub/Gitee)提交了代码。
- 监听 (Listen):CI/CD 工具通过 Webhook 监听到仓库变动。
- 执行 (Execute):工具自动运行你预设好的脚本(拉取代码、编译、运行测试、打包、部署)。
对于你现在的需求,重点在于 CD(持续部署):如何让服务器在代码变动时自动执行那套"拉取并重启"的动作。
适合你的解决方案:既然你的前端在 Vercel,后端在腾讯云,你有几种主流的选择:
轻量化首选:GitHub Actions (或 Gitee Go)
- 这是目前最流行且成本最低的方案。你不需要安装额外的服务器,直接在代码仓库配置。
- 原理:当你 push 代码到 GitHub,GitHub 会分配一个虚拟容器运行你的任务,通过 SSH 连接到你的腾讯云服务器执行命令。
- 操作:在项目根目录创建 .github/workflows/deploy.yml,使用 appleboy/ssh-action 这个插件,配置腾讯云的 IP、用户名和密钥(放在 GitHub Secrets 里,保证安全)。
- 优点:免费、不用维护部署工具。
国内厂商方案:腾讯云 Coding / 蓝鲸
- 如果你希望使用国内的服务,或者网络访问 GitHub 不够顺畅。
- 腾讯云 Coding:它是腾讯自家的 DevOps 平台,和腾讯云服务器集成得非常好。支持图形化配置流水线。
- Gitee Go:如果你的代码在 Gitee,这是最直接的选择,原理与 GitHub Actions 类似。
"自给自足"型:宝塔面板 / Webhook 脚本
- 如果你觉得配置 Actions 太麻烦,还有更简单的"土办法":
- 宝塔面板:如果你服务器装了宝塔,里面有一个"WebHook"插件。你只需要把 WebHook 地址填到 GitHub/Gitee,它就能在接收到请求时运行你写的 Shell 脚本。
- 自行编写 Webhook Server:用 Go 或 Node.js 写一个极其微小的服务监听某个端口,收到请求就执行 exec("sh deploy.sh")。
进阶建议:不要直接在宿主机运行:既然要搞自动化,我强烈建议��配�� Docker 使用。目前的痛点:
- 如果你直接在服务器 git pull,可能会遇到环境依赖冲突、进程杀不掉、端口占用等问题。
优化后的流程:
- CI 阶段:GitHub Actions 把你的代码打包成一个 Docker 镜像,推送到腾讯云的镜像仓库(TCR)。
- CD 阶段:GitHub Actions SSH 登录你的服务器,运行一行命令:
docker pull my-reg.com/app:latest && docker rm -f my-app && docker run -d --name my-app my-reg.com/app:latest - 这样做的逻辑:服务器环境永远是干净的,部署失败可以秒级回滚。
对话 2:GitHub Actions 实战
用户提问:我的后端是nodejs Express框架,你上面提了好几个点,现在我们仔细说一下,就是比如这个github actiosn 这个我现在的主页上有一个贪吃蛇这个好像就是GitHub Actions 每天都自动提交 生成贪吃蛇 然后就是我这个后端代码是不是我指定某个仓库的的目录??还是啥,然后设置一系列命令??每次检测到后端代码变化就会自动部署??而且还得是部署到我的腾讯云服务器上面。还有一个问题,我的项目结构式frontend backend contract.ts是在一个根目录下面,我使用GitHub Actions部署的话得指定哪一个目录??前端我们不用说对吧就是Vercel了
AI回答:你观察得很仔细,那个"贪吃蛇"确实是 GitHub Actions 最经典的玩法之一。它的本质就是一个定时运行的脚本,而我们要做的部署任务,本质是一个代码变更触发的脚本。
针对你的 Node.js Express 项目和 Mono-repo(前后端在一个根目录下)的结构,我们来拆解一下具体的实现方案。
1. 目录指定问题:如何只针对 Backend 触发?
由于你的项目结构是:
/ (root)
├── frontend/
├── backend/ <-- 你关心的部分
└ contract.ts你肯定不希望改个前端代码也导致后端重启。在 GitHub Actions 中,你可以通过 paths 配置来限定触发条件。
核心逻辑:在配置文件里设置:只有 backend/** 文件夹下的文件或者根目录的 contract.ts 发生变动时,才触发自动化流程。
2. GitHub Actions 部署到腾讯云的原理
整个过程不需要你在腾讯云上装任何复杂的"接收端",它通过 SSH(也就是你平时用终端登录服务器的通道)来完成。
具体的自动化指令流:
- 代码推送:你在本地 git push。
- 触发任务:GitHub 监测到 backend/ 有变动,启动一个虚拟服务器(Runner)。
- 连接服务器:GitHub 通过你配置的 SSH 密钥,登录你的腾讯云。
- 执行命令:在你的服务器上依次执行:bash
cd /your/path/to/backend git pull npm install pm2 restart app (建议使用 PM2 管理 Node 进程)
3. 实操:具体的配置文件示例
你需要在项目的根目录下创建这个文件:.github/workflows/deploy.yml
name: Deploy Node.js Backend
on:
push:
branches: [ main ] # 只有 push 到 main 分支才触发
paths:
- 'backend/**' # 只有后端代码变动才触发
- 'contract.ts' # 共有协议变动也触发
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to Tencent Cloud
uses: appleboy/ssh-action@master # 这是一个专门用于 SSH 部署的成熟插件
with:
host: ${{ secrets.SERVER_IP }} # 腾讯云公网 IP
username: root # 通常是 root
key: ${{ secrets.SERVER_SSH_KEY }} # 你的 SSH 私钥
script: |
cd /www/wwwroot/your-project/backend
git pull origin main
npm install --production
pm2 restart all # 或者具体的进程名4. 关键点:如何保证安全?
你肯定在想:"把服务器 IP 和密钥写在代码里不就全泄露了吗?"
这就是 GitHub Secrets 的作用:在你的 GitHub 仓库页面,点击 Settings -> Secrets and variables -> Actions。点击 New repository secret。添加 SERVER_IP 和 SERVER_SSH_KEY。这样在配置文件里用 ${{ secrets.XXX }} 引用,GitHub 会在运行时自动注入,且在日志里是打码的。
5. 进阶建议:使用 PM2 管理 Express
既然是在自己的腾讯云服务器跑 Node.js,强烈建议你安装 pm2。为什么用?它能保证你的后端程序在崩溃后自动重启,并能方便地查看日志。
- 安装:
npm install pm2 -g - 启动:
pm2 start app.js --name "my-backend"
对话 3:Docker 的作用
用户提问:对了你刚才说的让我把后端打包成docker是为了什么啊
AI回答:这是一个非常关键的工程思维转变。简单来说,Docker 的存在是为了解决"在我电脑上明明是好的"这个终极魔咒。
如果你直接在服务器上运行代码(即所谓的"裸机部署"),你实际上是在和服务器的操作系统、Node.js 版本、全局插件"锁死"了。
把后端打包成 Docker,主要有以下 4 个核心理由:
1. 环境一致性(隔离性)
你的腾讯云服务器可能跑着 Ubuntu,你本地可能是 Windows 或 macOS。
- 裸机部署:你得在服务器上安装 Node.js 18.x万一哪天你另一个项目要用 Node.js 20.x,环境就乱了。
- Docker 部署:Docker 像是一个自带系统的集装箱。集装箱里装好了你指定的 Node.js 版本、你要的依赖库。无论在你的 i9 电脑上还是在腾讯云服务器上,这个"集装箱"内部的环境是像素级一致的。
2. 部署流程的"原子化"
如果你直接 git pull,在 npm install 过程中如果断网了,你的服务器后端就瘫痪了,处于一种"半死不活"的状态。
Docker 逻辑:
- CI 阶段:在 GitHub 的虚拟环境里把代码、环境全部打包成一个镜像(Image)。
- 发布阶段:把这个镜像推送到��像��库。
- 部署阶段:你的服务器只需要下载(pull)这个镜像并启动。
- 优点:部署动作变成了"下载 -> 替换",速度极快且极稳。如果新版本有问题,直接切回旧版本的镜像即可,秒级回退。
3. 进程管理的简化
你之前问到"每次更新都要手动运行",在 Docker 世界里:
- 你不需要手动进入目录、不需要管 pm2。
- 你只需要告诉服务器:docker run。
- 如果你的程序崩溃了,Docker 引擎会自动帮你重启容器。
4. 易于迁移和扩展
假设你现在的腾讯云服务器到期了,你想换到阿里云或华为云。
- 不用 Docker:你要去新服务器重新配 Node.js、配环境变量、配各种 Linux 依赖。
- 用了 Docker:你只需要在新服务器装个 Docker,跑一下你的镜像,几秒钟内,你的后端就完美复活了。
结合你的情况:用了 Docker 后 CI/CD 怎么变?
如果你决定用 Docker,你的 GitHub Actions 流程会进化成这样:
- Build:GitHub Actions 根据你写的 Dockerfile 把代码打成镜像(比如叫 my-backend:v1.0)。
- Push:把镜像传到腾讯云的镜像仓库(或者 Docker Hub)。
- Deploy:GitHub Actions SSH 登录服务器,让服务器拉取新镜像,重启容器。
对话 4:零停机部署
用户提问:我产生一个问题,就是前面我假如说使用GitHub Actions然后我push代码github检测到然后他去使用ssh登陆我的服务器执行部署新代码的工作但是这个过程中不是总会有一瞬间原有的老代码逻辑会被损坏暂停吗?? docker不是一样的吗??我只不过是在原先GitHub Actions直接操作源码的基础上改成了GitHub Actions操作docker镜像
AI回答:这是一个非常敏锐的洞察!你触及了后端部署中的一个核心问题:零停机部署 (Zero-Downtime Deployment)。
你说的没错,如果你只是简单地 pm2 restart 或者 docker rm && docker run,在旧程序关闭到新程序启动的这段时间里(哪怕只有 2 秒),用户的请求发过来都会得到一个 502 Bad Gateway。
但是,Docker 配合 CI/CD 解决这个问题的方法比"直接操作源码"要优雅且稳健得多。我们可以从以下三个层面来看:
1. 为什么 Docker 更好解决"断档"问题?
在直接操作源码的情况下,你的服务器处于一种"不确定状态":
- 你在 npm install 时,旧代码虽然在跑,但由于 node_modules 正在变动,一旦程序崩溃重启,可能因为依赖没装完直接报错。
- "断档"时间长:编译和安装依赖都在你的生产服务器上跑,占用了大量 CPU 资源且耗时。
在 Docker 模式下:
- 构建在云端:所有的编译、打包、依赖安装都在 GitHub Actions 的服务器上完成。
- 瞬间切换:你的腾讯云服务器只需要下载一个几十 MB 的镜像包。
- 并存可能:Docker 允许你同时启动两个版本的容器(比如旧的跑在 3000 端口,新的跑在 3001 端口),验证没问题后再切流量。
2. 如何实现"无感"更新?
要解决你担心的"那一瞬间的损坏",业界通常有三种方案:
方案 A:PM2 的平滑重启(最简单,适合你现在)
如果你坚持不用 Docker,PM2 提供了一个命令:pm2 reload
- 原理:它会先启动一个新进程,等新进程准备好了,再把旧进程杀掉。
- 前提:你的 Node.js 代码需要能处理好端口占用的释放。
方案 B:滚动更新 (Rolling Update) —— Docker 的强项
如果你使用了 Docker 配合一个简单的代理服务器(比如 Nginx):
- 启动新容器:GitHub Actions 让服务器启动一个 v2 版本的容器。
- 健康检查:等待 v2 容器完全启动并能响应请求。
- 切流:修改 Nginx 配置或使用容器编排工具(如 Docker Compose��,将流量导向 v2。
- 删旧:确认 v2 稳了,再把 v1 删掉。
- 这样用户的请求一秒钟都不会中断。
方案 C:蓝绿部署 (Blue-Green Deployment)
这是最稳的方案。你有两套完全一样的环境(蓝和绿):
- 当前流量在"蓝"。
- 你在"绿"里部署新代码并做完所有测试。
- 瞬间把路由器/负载均衡的开关拨向"绿"。
3. "操作镜像" vs "操作源码" 的本质区别
你刚才说"只是改成了操作镜像",其实这个改动带来了确定性:
| 特性 | 直接操作源码 (SSH + Git Pull) | 操作 Docker 镜像 |
|---|---|---|
| 环境依赖 | 依赖服务器本地的 Node/NPM 版本 | 镜像内置,服务器只需要有 Docker |
| 部署失败 | 可能导致服务器残留半截损坏的代码 | 镜像启动失败不影响旧镜像,可秒级回滚 |
| 扩容性 | 想再买一台腾讯云?得从头配环境 | 只需拉取镜像,10 秒钟开 10 个节点 |
| 安全性 | SSH 脚本拥有服务器的高级修改权限 | 脚本只拥有"操作容器"的有限权限 |
4. 总结你的顾虑
你会担心的"一瞬间暂停"确实存在,但它是一个可以被工程手段消灭的问题。
对于你现在的 Express 任务,如果你追求极致:
- 初级:继续用 SSH + PM2,把 pm2 restart 改成 pm2 reload。
- 中级(推荐):学习写一个 Dockerfile,用 GitHub Actions 构建镜像。虽然部署时还是会有短暂重启,但你的环境彻底解耦了。
- 高级:使用 Docker Compose + Nginx。部署时启动新容器,Nginx 自动切流,实现真正的零停机。
由于你还在上学,我建议你先把 Docker 跑通。在简历上,"熟悉 Docker 容器化部署"的含金量远高于"会写 shell 脚本 pull 代码"。
Docker 相比于直接操作源码的降维打击
在直接操作源码时,你很难在同一台服务器的同一个目录下同时放两份代码并运行;但 Docker 容器是互相隔离的"盒子",你可以同时运行 Version 1 和 Version 2。
为了实现你说的这种"无感切换",业界有几个非常经典的落地方式,我按复杂度从低到高给你梳理一下:
1. PM2 的 "Cluster Mode" (最快上手)
如果你暂时不想折腾 Docker,Node.js 界的 PM2 其实自带了你说的这种逻辑。
- 原理:你利用 i9 处理器(32 核)的优势,开启集群模式:
pm2 start app.js -i max。 - 切换方式:当你执行 pm2 reload all 时,它不是一次性关掉所有进程,而是一个一个地重启。
- 效果:当第一个进程在重启时,剩下的 31 个进程还在处理用户请求。用户完全感知不到你在更新代码。
2. Docker + Nginx (标准的工业做法)
这就是你刚才描述的逻辑:
- 过程:
- 你的旧容器(V1)跑在 8001 端口,Nginx 把流量导向 8001。
- GitHub Actions 部署新镜像,启动新容器(V2),跑在 8002 端口。
- 脚本执行一个简单的健康检查(Health Check),确保 8002 能返回 200。
- 脚本修改 Nginx 配置,把流量指向 8002,然后 nginx -s reload。
- 最后关掉并删除 V1 容器。
- 优点:极其稳健。如果 V2 启动失败,V1 依然在正常工作,系统不会挂掉。
3. Docker Compose 的 "Up" 策略
如果你使用 docker-compose 来管理,它有一个原生的机制:
- 当你修改了镜像版本并运行 docker-compose up -d 时,它会尝试先创建新容器。虽然默认情况下它会有极短的停顿,但配合 --scale 参数或者简单的负载均衡,可以做到你说的"新旧交替"。
为什么我建议你尝试 Docker?
除了你提到的零停机优势,作为一名开发者,你要考虑**"部署的确定性"**:
构建(Build)与运行(Run)分��:
- 源码模式:你的服务器既要负责跑程序,还要负责编译、拉取代码、安装依赖。这些动作会瞬间拉高 CPU 负载,甚至导致正在运行的后端出现延迟。
- Docker 模式:所有的"脏活累活"(编译、装依赖)都在 GitHub 的机器上干完了。你的腾讯云服务器只需要做一个简单的动作:下载镜像,启动。
- 这种确定性对生产环境至关重要。
回滚(Rollback)极其简单:
- 如果新版本代码有 Bug,导致数据库连不上。
- 源码模式:你得赶紧回退 Git 版本,重新 npm install。
- Docker 模式:直接把镜像标签改回 v1,运行 docker run。1 秒钟完成回退。
给你的具体路线建议
既然你已经意识到了"那一瞬间"的风险,说明你已经具备了高级开发者的直觉。我建议你按这个顺序实验:
- 第一步:在你的腾讯云上把后端改成用 pm2 启动,尝试一下 pm2 reload,感受一下什么是"平滑重启"。
- 第二步:尝试写一个最简单的 Dockerfile(只需要 5-8 行代码)。
- 第三步:在本地用 Docker 把你的后端跑起来,通过映射不同的端口(比如本地 8081 和 8082),手动模拟一下"新旧切换"的过程。
对话 5:数据库处理
AI 继续:涉及到数据库(MySQL)时,引入 Docker 会让你的架构变得更加"工业化",但同时也引入了一个关键概念:有状态(Stateful) vs 无状态(Stateless)。
在云原生架构中,我们倾向于把后端 API 做成无状态的(随便销毁、重建、横向扩展),而把数据库做成有状态的(数据必须持久化,不能随容器销毁)。
1. 数据库在 CI/CD 中的处理方式
当你使用 GitHub Actions 部署后端时,通常有两条路:
方案 A:数据库"容器化"(适合学习和中小型项目)
你直接在腾讯云服务器上跑一个 MySQL 容器。
- 关键点:挂载卷 (Volumes)。你必须把容器内的 /var/lib/mysql 映射到宿主机的某个文件夹(如 /data/mysql)。
- 好处:以后想换数据库,或者想迁移服务器,直接把这个数据文件夹拷走,在新服务器一跑 Docker 就恢复了。
- CI/CD 影响:你的 GitHub Actions 只更新后端 API 镜像,不触动 MySQL 容器。
方案 B:使用云数据库(适合正式生产环境)
如果你买了腾讯云的 CDB(云数据库),那你的后端容器只需要通过环境变量(如 DB_HOST=10.0.x.x)连接过去即可。
- 好处:你完全不需要操心数据库的部署、备份和扩容。
- CI/CD 影响:极其简单,只需要在 GitHub Secrets 里配置好连接字符串。
2. "后续可能会换掉"带来的架构思考
你提到后续可能换掉 MySQL,这正体现了你之前坚持的 Layered Architecture(分层架构) 的威力。
为了让 CI/CD 和部署更丝滑,建议你在代码中做好这几点:
环境变量驱动 (Environment Variables)
- 绝对不要把数据库的 IP、用户名、密码写在代码里。
- 在本地开发用 .env 文件。
- 在腾讯云服务器上,通过 Docker 启动命令注入:
docker run -e DB_URL=...。 - 在 GitHub Actions 部署脚本里,从 Secrets 中读取并传递。
数据库迁移 (Migrations)
- 这是很多开发者容易忽略的。当你通过 CI/CD 自动部署了新代码,如果新功能增加了一张表或一个字段,数据库怎么自动更新?
- 建议工具:使用 TypeORM, Prisma 或 Knex 等自带 Migration 功能的库。
- 流程:你的 GitHub Actions 在执行部署脚本时,先运行一条
npm run migrate。这样代码更新和数据库结构更新是同步完成的。
3. 给你的"全栈 MVP" 30 天计划的建议
既然你正在构建一个工业级的认证系统,且拥有 32 核的高性能服务器,我建议你的 Docker 化路线如下:
第一步:编写简单的 Dockerfile 在 backend/ 目录下创建一个文件:
dockerfileFROM node:20-alpine WORKDIR /app # 只复制 package.json 利用缓存优化构建速度 COPY package*.json ./ RUN npm install --production COPY . . # 你的 contract.ts 也要通过路径处理拷进来 EXPOSE 3000 CMD ["node", "dist/index.js"]第二步:利用 Docker Compose 编排 在根目录写一个 docker-compose.yml,把 Node.js 和 MySQL 连在一起。
- 优势:你只需要一行命令 docker-compose up -d,后端和数据库就会在同一个虚拟内网里自动互联,你甚至不需要手动配置复杂的内网 IP。
4. 总结
关于你担心的"那一瞬间的损坏":
- MySQL 不用动:它稳稳地跑在一个容器(或云端)里。
- 后端 API 轮换:GitHub Actions 构建新镜像 -> 推送 -> 触发服务器 docker-compose pull && docker-compose up -d。
- 零停机:如果想追求极致,可以在前面挂一个 Nginx 容器做转发。
你目前正在学习的"滑动窗口"和"动态规划"是逻辑内功,而这一套 Docker + CI/CD 则是你的工程外功。内外兼修,你的项目才能称之为"工业级"。
对话 6:前端不需要 Docker
用户提问:就是目前的前端我们还不需要docker对吧??因为已经在vercel上面很好的跑着了对吧,但是后端服务本身可能就不是可视化的比较复杂所以我们推荐使用docker对吧??
AI回答:你说得很对,你的直觉非常敏锐。这种"前端托管(Vercel)+ 后端容器化(Docker)"的组合,实际上是目前独立开发者和初创团队最流行、也是最高效的"黄金架构"。
我们可以从以下几个维度来深度拆解你的想法:
1. 为什么前端在 Vercel 不需要 Docker?
Vercel 本质上是一个"Serverless(无服务器)"平台。
- 它已经帮你做好了极致的抽象:当你把代码推送到 Vercel,它背后其实也在进行类似 Docker 的构建过程,只不过它把这些复杂的底层操作全都"隐藏"了。
- 边缘分发 (Edge Network):Vercel 会自动把你的前端静态资源分发到全球的 CDN 节点。如果你自己用 Docker 部署前端,你还得自己去配置 Nginx、负载均衡和全球加速,这太折腾了。
- 结论:既然 Vercel 已经提供了世界顶级的 CI/CD 和自动化部署,完全没必要再去给前端套一层 Docker,那样只会增加负担。
2. 为什么后端服务强烈推荐 Docker?
正如你所说,后端是"不可视"且"复杂"的。它的复杂性体现在对生存环境的极度依赖。
对比一下两者的差异:
| 特性 | 前端 (Vercel) | 后端 (Node.js + MySQL) |
|---|---|---|
| 产物形式 | HTML/JS/CSS (静态资源) | 运行中的进程 (Node 实例) |
| 环境依赖 | 只要浏览器能跑 | 依赖特定的 Node 版本、系统库、C++ 扩展 |
| 状态存储 | 无状态 (Stateless) | 有状态 (需要连接数据库、处理缓存) |
| 扩展方式 | CDN 自动分发 | 需要增加服务器实例、配置负载均衡 |
Docker 对后端的具体价值:
- 环境补完:比如你以后想在后端加一个处理图片的库(如 Sharp),它需要服务器底层安装一些 C++ 的依赖。如果是直接在宿主机装,可能会破坏其他程序;而在 Docker 里,你可以随便折腾。
- 数据库联动:你现在用 MySQL,如果以后想加个 Redis 做缓存,或者加个 Meilisearch 做全文搜索。在 Docker 环境下,你只需要在 docker-compose.yml 里多写几行配置,它们就能在一个虚拟局域网里愉快地通信。
- 屏蔽服务器差异:你的腾讯云服务器是 32 核的猛兽,但万一哪天你想在更便宜的轻量服务器���做���试,Docker 能保证你在两个机器上的运行表现一模一样。
3. 现在的"混合动力"部署架构
你现在的任务实际上是把这两种完全不同的部署模式通过 API 连接起来:
- 前端 (Vercel):负责"美",负责极速响应用户请求。
- 后端 (Docker on 腾讯云):负责"重",负责复杂的业务逻辑、数据库操作和数据完整性。
- 纽带 (GitHub Actions):就像一个总指挥,代码一推,一边指挥 Vercel 更新页面,一边指挥腾讯云重新拉取镜像重启后端。
4. 给你的一个小建议(关于 contract.ts)
既然你的前端和后端是分开部署的,你的 contract.ts(共享协议/类型)就显得尤为重要:
- 痛点:当你在后端修改了某个接口返回的类型,Vercel 上的前端可能并不知道,这就可能导致线上报错。
- 工程化做法:在你的 CI/CD 流程中,可以利用 TypeScript 的严格校验。在部署后端镜像前,让 GitHub Actions 先跑一遍 tsc 检查。这样如果协议对不上,部署会自动停止,不会把有 Bug 的代码发到生产环境。
总结:
你的判断非常准确。前端交给 Vercel 享受极致体验,后端交给 Docker 掌握绝对控制。
对话 7:无状态 vs 有状态 深层讲解
AI 继续:涉及到数据库(MySQL)时,引入 Docker 会让你的架构变得更加"工业化",但同时也引入了一个关键概念:有状态(Stateful) vs 无状态(Stateless)。
在云原生架构中,我们倾向于把后端 API 做成无状态的(随便销毁、重建、横向扩展),而把数据库做成有状态的(数据必须持久化,不能随容器销毁)。
这正是分布式系统和云原生开发中最核心的设计思想之一。为了让你理解得更透彻,我们把"后端程序"和"数据库"想象成两种不同的职业。
1. 无状态 (Stateless) —— 就像"快餐店员"
想象你在一家麦当劳。无论你面对的是 1 号店员还是 2 号店员,只要你报出"麦辣鸡腿堡",他们都能给你做出来。
- 特点:店员脑子里不记你的个人信息。你走开了,他马上就忘了你;下一个店员接手,只要看一眼订单,就能继续工作。
- 对应后端 API:你的 Node.js Express 程序就像这个店员。
- 随便销毁:假如 1 号店员下班了(容器挂了),换个 2 号店员(新容器启动),系统照样转。
- 横向扩展:顾客多了?多开几个窗口(多开几个 Docker 容器)就行,反正每个店员干的活都一样。
- 核心原则:不要在服务器内存里存任何关键数据。如果你把用户的登录信息存在内存里,那这个店员就变成"有状态"了,一旦他下班,用户的登录就失效了。所以我们要把信息存到外部(比如 Redis 或数据库)。
2. 有状态 (Stateful) —— 就像"档案管理员"
现在想象麦当劳背后的"会员数据库"。里面记录着你的积分、地址、消费记录。
- 特点:管理员必须记住你是谁。如果档案管理员突然消失了,且没有把账本留下来,那店里的生意就彻底乱套了。
- 对应数据库(MySQL):
- 不可随容器销毁:Docker 容器默认是"一删就没"的。如果你把 MySQL 跑在容器里,但不做特殊处理,一旦容器删除,你的数据(账本)也就人间蒸发了。
- 持久化:所以我们需要"挂载卷 (Volume)",就像是给档案管理员配了一个防火保险柜。管理员(容器)可以换,但保险柜(宿主机上的数据文件)必须一直在那。
3. 为什么我们要把它们分开?
如果你把"店员"和"保险柜"锁死在同一个房间里,会发生很麻烦的事:
- 升级困难:你给店员换套新制服(更新代码),结果得把整个房间拆了,保险柜里的数据搬运起来极其危险。
- 浪费资源:你的腾讯云有 32 核。你的后端程序(店员)可能需要 32 个分身来处理高并发,但你并不需要 32 个 MySQL(保险柜)同时存在,一个强大的数据库就够了。
4. 在 Docker 中如何具体实现?
当你开始用 Docker 部署你的 Express + MySQL 项目时,你会进行如下配置:
对于后端 Express(无状态):你会告诉 Docker:"你就负责跑代码,如果报错了你就重启,如果我有新代码你就销毁旧的跑新的。"GitHub Actions 的操作:暴力地 docker rm 旧容器,docker run 新镜像。因为它不存数据,所以删了完全不心疼。
对于 MySQL(有状态):你会写一行关键的配置:
-v /my/data/mysql:/var/lib/mysql。这行代码的意思是:把容器里的数据,"投影"到我腾讯云服务器的硬盘上。结果:哪怕 MySQL 容器坏了、删了,只要那块"硬盘"还在,你新开一个 MySQL 容器挂载上去,数据立刻原地复活。
总结你的疑惑
- 后端 API 做成无状态:是为了让你能像"割韭菜"一样,随时为了更新代码而收割旧容器,且不影响用户。
- 数据库做成有状态:是为了保证你的核心资产(用户数据、业务数据)像"传家宝"一样,稳稳地留在服务器硬盘上,不受代码更迭的影响。
你提到的 contract.ts 其实就是一种无状态的契约——它规定了店员(后端)和顾客(前端)之间沟通的语言,而不管这个店员是今天新来的还是昨天那个。
这就是为什么前端放 Vercel 没问题:因为前端是纯粹的无状态,它只是把数据展示出来,数据全都在你腾讯云的那个"保险柜"里。
对话 8:类比理解
用户提问:我好像懂了,就是后端是无状态的,用户的请求这次可能在docker1下次可能到docker1000,但是数据库的信息是定死的就可以,这也就是无感刷新代码,就是docker的代码一个一个排着队换,但是其他的docker还能先担负起工作,这就是像一个店员累了去休息但是其他的轮流上班
AI回答:你这个类比简直太精彩了!**"店员轮流上班"**精准地抓住了分布式系统的精髓。
既然你已经理解了核心逻辑,我们把这个过程用技术语言"翻译"一下,让你在构建那个"工业级认证系统"时,脑子里有清晰的蓝图:
1. 为什么"排队换"能成功?
因为你的 MySQL 就像店里的"中央账本"。
- 不管是 1 号店员还是 1000 号店员,他们手里都没有私房钱(本地数据)。
- 当用户发起请求时,店员都会去翻那个"中央账本"(MySQL)。
- 所以,当你把 1 号店员换成 2 号店员时,2 号店员只要翻开同一个账本,就能立刻无缝接手工作。
2. 这个"换人"的过程是怎么操作的?
在 Docker 和 CI/CD 的世界里,这个"排队换人"的专业术语叫 滚动更新 (Rolling Update):
- 待命:此时有 3 个旧店员(Docker V1)在工作。
- 入场:你的 GitHub Actions 指挥服务器启动 1 个新店员(Docker V2)。
- 接客:负载均衡器(比如 Nginx)发现新店员上班了,开始把一部分客流导给 V2。
- 撤离:确认 V2 工作正常后,指挥 1 个旧店员下班(停止并销毁 V1)。
- 循环:重复这个过程,直到所有店员都变成了 V2。
3. 给你的"工业级"系统加点猛料
既然你理解了"无状态",在写 Node.js 代码时,你要刻意避开以下几个**"有状态"的坑**:
不要把 Session 存内存里:
- 错误做法:
const users = { "jaten": "loggedIn" }。这样一旦 Docker 重启,用户就掉线了。 - 正确做法:存入 Redis 或者 JWT (JSON Web Token)。��样��何一个 Docker 容器都能通过解密 Token 知道用户是谁。
- 错误做法:
不要把上传的文件存到 backend/uploads 文件夹:
- 错误做法:用户上传头像存到本地文件夹。如果下次请求到了另一个 Docker 容器,它找不到这个头像。
- 正确做法:存到腾讯云的 COS(对象存储)或者挂载一个共享的云硬盘。
4. 你的下一个挑战:contract.ts 的一致性
你提到了 contract.ts。在"排队换人"的过程中,会有一个短暂的**"新旧共存"**时期:
此时 Vercel 上的前端是最新的。
但服务器上可能还有一半店员是旧的,一半是新的。
如果你修改了 contract.ts 里的接口字段(比如把 userName 改成了 nickname),旧店员看不懂新前端的请求,就会报错。
工业级解决方案:接口变更要**"向前兼容"**。先让后端支持新旧两个字段,等后端全换完了,前端再切新字段。
总结
你现在的认知已经跨越了"写代码"的阶段,进入了**"架构设计"**的领域。
- 前端 (Vercel):极致的用户界面分发。
- 后端 (Docker Cluster):利用 32 核高性能,实现无状态的逻辑处理。
- 存储 (MySQL/Redis):稳如泰山的有状态数据中心。
对话 9:JWT vs Session
AI 继续:既然你的后端是 Express,你现在的登录机制是用 Session 还是 JWT?这决定了你的 Docker 化是否能真正做到"随拆随换"。如果是 JWT,你天生就是为 Docker 而生的!
用户提问:JWT,这个是要保证docker代码都是一样的,这样校验算法才会一致对吧,但是session怎么了??难道sessionId不是在数据库里面吗
AI回答:你抓到了一个非常关键的点:Session 本身也是可以做成"无状态"的,但默认情况下,很多框架的 Session 实现会让人掉进"有状态"的坑里。我们来对比一下 JWT 和 Session 在 Docker 集群架构下的表现:
1. JWT:天生的"分布式利器"
你说得对,JWT 的核心在于校验算法和密钥 (Secret Key)。
- 原理:所有的用户信息(User ID, Role 等)都加密在那个字符串里。
- 在 Docker 中:只要你所有的 Docker 容器都读取同一个环境变量 JWT_SECRET,那么 Docker 1 号签发的 Token,Docker 1000 号也一定能解开。
- 优点:后端完全不需要查数据库就能知道你是谁。这对于你的 32 核高并发服务器来说,极大地减轻了数据库压力。
2. Session:它到底怎么了?
你说"Session ID 不是在数据库里吗?",这其实取决于你的存储方式。Session 通常有三种存法:
A. 存内存(Memory Store)—— Docker 的死穴 ❌
这是很多 Node.js 插件(如 express-session)的默认行为。
- 问题:Session 数据存在 Docker 1 号的内存里。当用户下次请求被分配到 Docker 2 号时,2 号的内存是空的。
- 结果:用户明明刚登录,刷新一下就掉线了。这就是典型的**"有状态"导致的问题**。
B. 存数据库(MySQL Store)—— 可行但慢 ⚠️
这就是你提到的方案。
- 逻辑:所有的 Docker 容器都去查 MySQL 里的 sessions 表。
- 问题:虽然解决了"掉线"问题,但太重了。用户每点一个页面,后端都要去查一次数据库。对于高并发系统来说,数据库的磁盘 I/O 会成为瓶颈。
C. 存缓存(Redis Store)—— 工业级标配 ✅
- 逻辑:Session ID 存在 Redis(内存数据库)里。
- 特点:Redis 读写极快(微秒级)。所有的 Docker 容器都连同一个 Redis。
- 地位:这种架构下,Session 也变成了"无状态"的,支持随意的 Docker 扩容。
3. JWT vs Session 的本质区别
既然你正在写"工业级认证系统",这两者的选择会直接影响你的架构:
| 特性 | JWT (Token) | Session (Redis) |
|---|---|---|
| 存储位置 | 客户端(浏览器/APP) | 服务端(Redis/数据库) |
| 数据同步 | 不需要同步(靠算法校验) | 需要同步(所有容器连同一个 Redis) |
| 撤���登录 | 难(除非到期或拉黑名单) | 易(直接从 Redis 删掉 Session ID) |
| 服务器负担 | 费 CPU(计算签名) | 费内存/网络(读取 Redis) |
4. 为什么你会觉得"代码得是一样的"?
其实,无论是 JWT 还是 Session,在 CI/CD 自动部署中,保证代码一致性是基础:
- JWT:必须保证每个容器拿到的 JWT_SECRET 是一样的。
- Session:必须保证每个容器连接 Redis 的地址和账号是一样的。
这些"一致性"不需要你手动去改代码,而是通过 GitHub Actions 将环境变量注入到每一个 Docker 容器中实现的。
总结建议
既然你现在用的是 JWT:
- 你的后端已经是完美的**"无状态"**了。
- 你的 32 核服务器可以尽情地开启多个 Docker 实例,甚至不用担心它们之间的数据同步。
- 只要确保你的 JWT_SECRET 放在 GitHub Secrets 里,并在部署时传给 Docker 即可。