前言:
以前部署一个项目,基本就是在本地改完代码,然后手动打包、上传服务器、拉取最新代码、重新构建镜像,最后再启动服务。项目刚开始的时候这样做问题不大,但随着修改次数变多,每次上线都要重复一遍相同的操作,既麻烦,也容易因为某个步骤遗漏导致部署失败。
这次就想着把整个过程自动化起来:代码提交到 GitHub 后,由 GitHub Actions 自动触发构建流程,完成代码检查、Docker 镜像构建,并进一步连接服务器完成部署。这样以后只需要正常提交代码,后面的构建和上线交给 CI/CD 流程处理。
这篇文章主要记录这套流程从零搭建的过程,包括 GitHub Actions 的配置、服务器环境准备、Docker 镜像构建、SSH 部署以及自动更新服务等内容。相比单纯介绍 CI/CD 的概念,这里更侧重实际操作和过程中遇到的问题,希望以后自己再次配置类似环境时,可以直接按照这篇记录重新走一遍。
📝 项目背景与最终目标
这次 CI/CD 实验没有直接从复杂项目开始,而是先做了一个很小的静态网页,用它把 Git、GitHub、GitHub Actions、SSH、Docker、Docker Compose 和 Linux VPS 串起来。
这样做的好处是,每一层出了问题都比较容易定位。网页本身没有复杂业务逻辑,部署失败时基本可以判断是 Git、Actions、SSH、Docker、Compose 或服务器环境的问题,而不是应用代码本身。
本次项目的本地目录是:
最终项目结构:
其中:
frontend/保存网页代码。
Dockerfile负责说明 Docker 镜像如何构建。
docker-compose.yml负责说明容器如何运行。
.github/workflows/ci.yml负责 CI。
.github/workflows/ssh-test.yml后来改成了 CD 工作流,负责自动连接 VPS 并部署。
整个流程最终形成:
这里需要区分 CI 和 CD。
CI(Continuous Integration,持续集成)主要解决的是“代码提交以后有没有问题”。本次实验中,CI 会检查项目文件是否存在,并尝试构建 Docker 镜像。
CD(Continuous Deployment,持续部署)解决的是“代码确认没有明显问题以后,怎么自动更新到服务器”。本次实验中,CD 通过 GitHub Actions 使用 SSH 连接 VPS,然后在服务器上拉取最新代码并重新构建、启动 Docker Compose。
因此,单独执行
git push 只是把代码提交到了 GitHub;真正形成 CI/CD,是因为 GitHub Actions 接收到 push 事件以后继续自动执行检查和部署。本地项目与 Docker 部署
先准备一个最小可运行项目
最开始没有直接做后端服务,而是使用 Nginx 提供静态网页。
frontend/index.html 最初是一个简单页面:style.css 和 script.js 分别负责页面样式和按钮交互。最初的网页比较简单,后面为了让部署后的页面更像一个正式的 Demo,又把前端改成了一个深色科技风的 CI/CD 展示页面,增加了自动部署状态、Pipeline、Docker 等内容。
这里有一个值得注意的地方:网页怎么改并不影响 CI/CD 的基本流程。只要代码进入 Git 仓库,后面的自动化过程都可以保持不变。
编写 Dockerfile
项目根目录下创建
Dockerfile:这个 Dockerfile 虽然很短,但已经包含了一个完整 Docker 镜像的基本构建过程。
表示以
nginx:alpine 作为基础镜像。Alpine 版本比较小,适合这种简单实验。设置工作目录。Nginx 默认会从这个目录提供静态文件。
把项目中的
frontend/ 目录复制到 Nginx 的网页目录。说明容器中的应用使用 80 端口。它更多是一个镜像层面的声明,并不会自动让宿主机开放 80 端口。
让 Nginx 在前台运行。Docker 容器需要有一个持续运行的前台进程,否则容器会退出。
本地第一次构建和运行
先进入项目目录:
构建镜像:
这里:
docker build表示构建镜像。
t ci-cd-demo:v1给镜像设置名称和标签。
.表示使用当前目录作为 Docker build context。
构建成功以后,可以查看镜像:
然后使用
docker run 启动:这里的:
含义是:
访问:
可以看到网页。
这一步主要是为了先证明 Dockerfile 本身能够正常构建和运行。
从 docker run 过渡到 Docker Compose
手动
docker run 可以运行容器,但参数比较容易越来越多。后面改成使用 Compose 管理。docker-compose.yml:这里没有使用
version: 字段,因为新版 Docker Compose 已经不需要这个字段。主要配置:
表示 Compose 不直接拉一个现成应用镜像,而是根据当前目录中的 Dockerfile 构建。
指定容器名称,方便后面排查。
把宿主机 8080 映射到容器 80。
让 Docker 在容器异常退出或 Docker 服务重启后自动尝试恢复容器,除非人为停止。
使用 Compose 启动:
其中:
up:创建并启动服务。
d:后台运行。
-build:启动前重新构建镜像。
查看运行状态:
到这里,本地的 Docker + Compose 部分已经验证完成。
Git 与 GitHub 仓库
Docker 能正常运行以后,再把项目纳入 Git 管理。
进入项目:
初始化 Git:
查看状态:
第一次提交:
然后把主分支统一命名为
main:接下来在 GitHub 创建仓库,然后添加远程仓库。
本次仓库使用 SSH 地址:
添加:
第一次推送:
成功以后,本地代码就进入 GitHub。
这一阶段的关键认识是:Git 和 GitHub 是两个不同的概念。
Git 负责本地版本管理、提交、分支等操作;GitHub 是远程 Git 仓库,同时还提供 GitHub Actions 等自动化能力。
后面 CI/CD 能自动运行,是因为代码被 push 到 GitHub 以后,GitHub Actions 可以监听这个事件。
CI:使用 GitHub Actions 自动检查项目
项目进入 GitHub 后,开始配置 CI。
在:
中写入:
这份配置可以拆开理解。
表示
main 分支发生 push 时触发。表示针对
main 分支的 Pull Request 也可以触发 CI。表示这个 Job 运行在 GitHub 提供的 Ubuntu Runner 上。
第一步:
把当前 GitHub 仓库的代码下载到 Runner。
第二步:
检查关键文件是否存在。
这不是复杂的自动化测试,但对于当前 Demo 很合适,因为首先需要确认项目结构没有被破坏。
第三步:
尝试构建 Docker 镜像。
这一点很重要,因为 CI 不只是检查 Git 文件是否存在,还提前验证 Dockerfile 能不能构建。
整个 CI 的意义可以理解为:
如果 CI 失败,就应该先解决问题,而不是直接认为代码可以部署。
SSH:让 GitHub Actions 和 VPS 能互相完成工作
真正做 CD 之前,需要解决两个不同方向的 SSH 问题。
这次特意使用了两套 SSH Key,因为它们解决的是完全不同的事情。
GitHub Actions → VPS
GitHub Actions 需要主动登录 VPS,所以在 VPS 上生成了一套专门给 GitHub Actions 使用的密钥:
公钥加入:
私钥则保存到 GitHub 仓库的 Actions Secrets 中。
配置了:
其中:
VPS_HOST:VPS 公网 IP。
VPS_USER:登录用户,本次使用root。
VPS_SSH_KEY:GitHub Actions 登录 VPS 使用的私钥。
之后使用
appleboy/ssh-action@v1 测试。测试成功时日志中出现了:
这一步非常重要,因为它证明:
这条链路已经打通。
VPS → GitHub
但是 CD 还有另外一个方向。
GitHub Actions 登录 VPS 后,VPS 需要执行:
因此 VPS 自己也需要能够访问 GitHub。
一开始直接:
出现:
这不是 GitHub Actions 的 Key 有问题,而是因为 VPS → GitHub 这一方向没有认证。
于是又在 VPS 上生成了一套独立的密钥:
把
/root/.ssh/github.pub 添加到 GitHub 账户的 SSH and GPG keys 中。然后测试:
成功返回:
这句话的含义不是失败。
GitHub 本身不提供普通 SSH Shell,但已经确认 GitHub 成功识别了这把 SSH Key。
为了以后不需要每次手动指定:
又配置了:
内容:
然后:
这样 VPS 以后执行:
就可以自动使用这把 Key。
在 VPS 上准备项目目录
最终把项目放在:
也就是说,服务器上的工作目录大致是:
这里的逻辑是:
GitHub Actions 本身并不需要把整个项目文件通过 SSH 传过去。
它只需要:
- SSH 登录 VPS。
- 进入项目目录。
git pull获取最新代码。
- 使用最新代码重新构建并启动容器。
CD:把部署真正自动化
前面的 CI 和 SSH 都验证以后,开始配置 CD。
原本用于 SSH 测试的:
后来直接改成真正的 CD 工作流:
这里的执行过程非常清楚。
第一步:push 触发
只要代码 push 到
main,CD 就会开始。第二步:GitHub Actions 登录 VPS
使用前面配置好的:
建立 SSH 连接。
第三步:进入服务器项目目录
第四步:拉取最新代码
这一条命令把 GitHub 上刚刚 push 的代码同步到服务器。
实际测试时出现过一次:
说明 VPS 成功从 GitHub 拉到了新的提交。
第五步:重新构建并启动
因为网页代码发生变化,所以需要重新构建镜像。
完整过程:
第六步:查看容器
用于确认服务是否正常运行。
第一次真正部署时遇到的问题:8080 端口冲突
第一次 CD 部署时,GitHub Actions、SSH、Git Pull、Docker Build 都成功了,但是容器启动失败:
这说明问题已经不是 GitHub、SSH 或 Dockerfile,而是 VPS 上已经有其他程序占用了 8080。
当时没有直接执行停止命令,而是先要求检查:
以及:
原因是 VPS 上还有其他正在运行的服务,不能为了部署 Demo 就随便停止某个容器。
最终决定把 Demo 的宿主机端口改成 80。
Compose 改成:
这里仍然是:
修改后再次:
GitHub Actions 再次触发 CD,最终部署成功。
这个问题很值得记录下来,因为实际工作中部署失败并不一定是代码问题。端口被占用、磁盘不足、权限错误、Docker 网络问题等,都可能导致部署失败。
一个容易忽略的问题:部署失败时 Workflow 可能仍显示成功
第一次部署时还发现了一个脚本层面的隐患。
虽然:
已经报错,但是后面的:
仍然继续执行。
因此 GitHub Actions 最终可能把整个 SSH Step 当成成功。
这说明当前 Workflow 还不够严谨。
后续应该在脚本开头增加:
例如:
这样只要其中一个命令返回非 0 状态,脚本就会停止,GitHub Actions 才能正确显示部署失败。
这也是这次实验中比较重要的一点:自动化不是把几个命令串起来就结束了,还需要保证失败能够被正确传递。
最终验证自动部署
CD 成功以后,最重要的不是只看 GitHub Actions 绿色,而是验证完整闭环。
下一次修改网页内容,例如修改:
为:
然后本地:
这时候不再登录 VPS 手动执行:
也不再手动执行:
而是直接等待:
然后访问服务器的 80 端口,确认页面已经发生变化。
到这里,整个实验才真正完成了“自动部署”的验证。
最终流程、问题记录与后续改进
这次实验最终形成的基础 CI/CD 架构可以概括为:
整个实验过程中实际遇到的问题主要集中在以下几个方面。
Docker 与服务器环境问题
Dockerfile 本身可以构建,但服务器上的 Docker 环境和本地并不完全一样。
尤其是服务器拉取:
速度比较慢,所以 Docker Build 在 VPS 上花费的时间明显比本地长。
这说明“本地能跑”并不代表“服务器一定能快速部署”。实际项目还需要考虑镜像缓存、镜像仓库、网络环境等问题。
SSH 是两个方向的问题
这次最容易混淆的地方之一就是两套 SSH Key。
实际关系是:
以及:
两者不能混为一谈。
前者解决:
后者解决:
这两个方向都打通以后,CD 才能顺利执行。
GitHub Actions Secret 的作用
VPS 地址、登录用户和私钥没有直接写到公开的 Workflow 中,而是使用:
这样可以避免把 SSH 私钥直接写进 Git 仓库。
特别是 SSH 私钥、Token、密码等内容,不能提交到 Git。
如果密钥意外暴露,应当及时更换,而不是继续使用已经泄露的凭证。
端口冲突的排查思路
第一次部署失败时:
正确的思路不是直接执行:
或者随便:
而是先确认是谁占用了端口:
确认之后,再决定是释放端口,还是修改项目端口。
对于一台已经运行多个服务的 VPS,这一点尤其重要。
当前方案为什么适合学习,但还不算最完善
现在的 CD 是:
它的优点是简单,能够把 CI/CD 的核心流程完整跑通。
但它也有一个比较明显的问题:每次部署都要在 VPS 上重新构建镜像,而且 VPS 还需要访问 Docker Hub 拉取基础镜像。
更进一步的做法可以变成:
这样 VPS 主要负责运行容器,不负责完整的镜像构建。
不过对于第一次学习 CI/CD 来说,目前这种:
反而更容易理解每一步到底发生了什么,因此适合作为第一版。
这次实验真正需要掌握的并不是某一条命令,而是整个部署思路:
以后换成 FastAPI、Node.js、Java、前后端分离项目,甚至更复杂的服务,只是 Dockerfile、Compose 和测试命令发生变化,整个 CI/CD 的基本思想仍然一样。
补充:密钥生成和部分关键步骤
有关CI/CD的问题,欢迎您在底部评论区留言,一起交流~
- Author:迷途
- URL:http://blog.ortech.nyc.mn/%E7%9F%A5%E8%A1%8C%E5%90%88%E4%B8%80/cicd
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts






