OYLZ
博客游戏OY_UIPixelForge
备案信息: 粤ICP备18091077号-1

记一次 Jenkins 部署

2026/10/3 3:34:0
服务器 ,运维 ,Jenkins ,Docker

我有一台阿里云的 99 元服务器,2 核 2 GiB,平时用来跑自己的几个项目。后来想把部署流程整理一下,提交代码后点一下就能发布,于是在服务器上装了 Docker 和 Jenkins。

安装时先遇到镜像下载、插件安装的问题。好不容易把项目接上,构建又经常把服务器拖得很慢,有时连已经在运行的网站都无法访问。尝试限制资源后,效果仍然不太理想。

最后的处理方式是:在本地电脑的 Docker 中运行 Jenkins,把安装依赖和打包都放到本地,构建完成后再把产物发到服务器。服务器只负责运行项目。

这里把中间的过程和几个具体问题整理一下。

先把 Jenkins 装起来

最开始卡在下载镜像。服务器直接拉取不顺利,换镜像源也没有立刻解决,腾讯云的也试过拉不下来,还考虑过使用 GHCR。

最后用了一个比较直接的办法:本地下载,再导入服务器。

在 Windows 上安装 Docker Desktop,本地执行:

docker pull jenkins/jenkins
docker save -o jenkins.tar jenkins/jenkins:latest

然后把 jenkins.tar 上传到服务器,在服务器导入:

docker load -i jenkins.tar

这里没指定标签的 docker pull jenkins/jenkins 拉的是 latest。

服务器上同时留着几个 Jenkins 镜像,有新的 latest,也有创建时间很早的旧镜像。所以镜像名字里写着 lts,并不能说明本地这份镜像就是当前的 LTS。

最终安装页面显示的版本是 Jenkins 2.562,运行容器使用了 jenkins/jenkins:2.562-local 这个标签。

镜像导入后,Jenkins 能启动了,接下来又卡在插件安装。前面的插件很快装完,后面的 Pipeline、Git 等插件一直转圈,等了十几分钟没有明显进展。

下载 Jenkins 镜像和安装 Jenkins 插件是两件事。镜像离线导入成功,只解决了 Jenkins 本体的安装,插件及其依赖仍然需要另外下载。

第一个项目,卡在基础镜像下载

第一个接入的项目是雪碧图生成工具。

早期的任务比较简单,在 Jenkins 的“执行 shell”里写构建和启动命令,大致是:

拉取 Git 仓库
    ↓
docker build 构建项目镜像
    ↓
停止并删除旧容器
    ↓
docker run 启动新容器

构建过程中,获取基础镜像失败了:

failed to resolve source metadata for docker.io/library/node:22-alpine
lookup hub-mirror.c.163.com ... no such host

日志显示,构建已经执行到了 Dockerfile,但获取 node:22-alpine 的镜像信息时,配置中的镜像源域名解析失败。

这和前面下载 Jenkins 镜像的问题又是分开的。Jenkins 已经能运行、Git 仓库也能拉下来,不代表项目 Dockerfile 里需要的 Node、Nginx 基础镜像就一定能下载。

处理下载问题后,构建继续往下走,第一个项目终于构建成功,容器也启动了。

所以,这台机器确实能完成构建。后面的问题是,构建和网站一起跑时,使用体验很不稳定。

构建成功后,网站还不一定能访问

第一个项目的容器端口映射是 5173:80。当时安全组没有放行 5173,外部直接访问还要检查网络入口,因此需要先在服务器内部验证服务。

排查时,也要区分几个不同的状态:

  • Jenkins 是否构建成功。
  • 容器是否还在运行。
  • 服务器本机能否访问容器端口。
  • 域名经过 Nginx 后能否正常访问。

接入宿主机 Nginx 时,又遇到过端口占用:

bind() to 0.0.0.0:80 failed
Address already in use

查监听进程,80 端口上已经有 Nginx。但执行重载时,又提示:

open() "/run/nginx.pid" failed

通过 systemd 重载则是:

nginx.service is not active, cannot reload

当时出现了“进程在运行,但服务管理状态不正常”的情况,排查过程中还要区分宿主机 Nginx 和项目容器里的 Nginx。仅看见一个 Nginx 进程,并不能说明当前执行的管理命令就在控制它。

第二个项目 pixel-forge 又遇到了另一个问题:Jenkins 显示打包成功,执行 docker ps 却看不到新容器。

换成 docker ps -a,才发现容器已经退出:

pixel-forge-web-container   Exited (1)

日志里写得很清楚:

"worker_processes" directive is not allowed here
in /etc/nginx/conf.d/default.conf:2

配置里的全局指令被放到了不允许它出现的上下文中,Nginx 启动失败,容器随即退出。

这两次问题让我开始把“构建”和“运行”分开看。镜像构建成功,只能说明镜像生成了;启动命令执行过,也还要继续检查容器状态和实际访问结果。

限制资源后,构建还是不好用

项目接上后,真正影响日常使用的是服务器资源。

服务器本来就在跑网站,再加上 Jenkins 和项目构建,安装依赖、编译、打包时就会和线上服务争抢资源。有时构建很慢,有时整台服务器没有响应,连网站也打不开。

查找低配置服务器运行 Jenkins 的优化方案时,主要有几个方向:减少并发、增加 swap,以及限制 Jenkins 的 JVM 堆。例如:

JAVA_OPTS="-Xms256m -Xmx1024m"

这里的 -Xmx 限制的是 Java 堆,不能把它理解成“整个 Jenkins 加构建最多占用这么多内存”。Node 构建进程、Docker 构建过程中启动的进程,以及 Java 自身的其他内存开销,都不能简单算进这个上限。Java 参数说明

后来也尝试过限制资源占用,但实际效果仍然不好:要么构建很慢,要么还是会影响网站。

限制资源可以约束某个进程,但项目需要完成的编译工作并没有因此减少。如果给构建留的资源太少,它就可能变慢或失败;留得多,又会挤占线上服务的空间。

我需要的只是方便发布自己的项目。如果每次点发布,都要担心网站跟着失去响应,这套流程就很难继续用下去。

把构建搬到本地

后来迁移服务器时,部署方式也一起调整了。

新的分工是:

本地电脑
    Jenkins 拉取代码
        ↓
    在本地容器中安装依赖、构建项目
        ↓
    SSH / rsync 传输产物
        ↓
云服务器
    接收产物
        ↓
    Nginx 提供静态文件
    Docker 运行前后端服务

本地 Jenkins 跑在 Windows 的 Docker Desktop 中。构建需要哪个 Node 版本,就使用对应的构建容器。

Jenkins 的任务、凭据和构建记录放在 jenkins_home 数据卷里。定制镜像补上了 Docker CLI、SSH、rsync 等工具,并通过挂载 Docker socket 调用本机的 Docker。

服务器端则明确不再执行项目的依赖安装和构建,只接收已经准备好的运行产物。

不同项目传过去的内容也不一样:

项目类型本地处理服务器处理
普通静态前端安装依赖,生成 distNginx 提供静态文件
Next.js 博客构建并组装 standalone 产物Node 容器运行 server.js
后来接入的微服务构建 Docker 镜像并导出加载镜像,启动服务

所以,本地使用 Docker 构建,并不意味着所有项目都要上传一个完整镜像。静态站直接传构建目录就可以,服务器连前端构建工具都不需要准备。

这里和最开始的离线安装有一个区别:最初只是让本地电脑帮忙下载 Jenkins 镜像,Jenkins 仍在云端运行;这次则是把 Jenkins 和实际构建工作一起迁了回来。

本地构建和远程运行之间,还需要补几处

搬到本地后,构建压力的问题解决了,但发布链路也需要适配。

Next.js 使用 standalone 产物

博客前端用了 Next.js,把构建结果单独部署时,配置了:

output: 'standalone'

构建后会生成 .next/standalone,包含运行所需的文件和选取出的依赖。静态资源还需要另外补进去,流水线中有这样的组装步骤:

cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static

然后把组装好的目录同步到服务器,由 Node 容器运行其中的 server.js。服务器不需要再安装一遍完整依赖,也不需要重新执行 next build。Next.js 14 standalone 说明

容器里的 localhost 需要重新检查

迁移后还出现过:博客接口直接访问有数据,但首页服务端渲染出来的文章列表是空的。

原来的请求地址写着 localhost:5101。前后端在同一台宿主机上直接运行时,这个地址可以使用;拆成不同容器后,前端容器里的 localhost 指向前端容器自身,访问不到另一个后端容器。

后面改为通过环境变量指定后端地址,使用 Docker 网络中的后端容器名访问。

这类问题和构建是否成功没有直接关系。产物可以正常生成,但运行环境变了,原来成立的地址就不一定还成立。

传输失败要能让任务失败

部署阶段还踩过一个不明显的问题。

为了只显示传输日志的最后几行,脚本把 rsync 输出接到了 tail。这样如果前面的传输失败,而最后的 tail 正常结束,就可能掩盖传输错误。

后来在相应的 Bash 脚本中增加了 set -o pipefail,让管道前面的失败也能传回流水线。

否则会出现一种很难受的情况:任务显示成功,服务器上的文件却没有更新。

后来真的遇到了“发布成功,网站还是旧的”

博客还出现过一次类似现象。

Jenkins 拉到了新提交,构建成功,rsync 执行了,容器也重启了,但网站看起来还是旧版。

最后发现是两个目录没有对上:

Jenkins 同步目标:standalone
容器实际挂载:   standalone.new

新产物一直写进 standalone,运行容器却在读取另一个旧目录。重启容器只会让它重新运行旧文件。

修正挂载目录后,再检查公网页面引用的资源文件,才确认这次构建真正在线上生效。

这次的问题很具体:构建和传输都成功了,但运行容器没有使用它们的结果。发布验证需要一直检查到网站实际返回的内容。

现在的发布方式

本地构建方案先跑通了第一个项目,后面继续处理 GitHub 网络、项目依赖和脚本转义问题,最终六个发布任务全部完成了验收。

云端旧 Jenkins 被停用,服务器另外增加了 2 GiB swap。swap 提供了一些内存缓冲,主要变化还是把依赖安装和编译从线上机器移走了。

现在发布项目,操作仍然很简单:

提交代码 → push → Jenkins 点 Build Now

静态站同步产物后生效,动态服务由部署任务重启或重建运行容器。服务器保留运行网站需要的工作,不再承担项目构建时的资源峰值。

这个方案也有一个直接的前提:发布时,本地电脑、Docker Desktop 和 Jenkins 都需要运行。电脑关机后不能发起新的本地构建,但已经部署在服务器上的网站照常运行。

对我这些个人项目来说,这样就够用了。保留了点一下发布的便利,也解决了之前构建经常拖垮网站的问题。