为什么个人站也该有 CI/CD,而不只是 git pull
很多个人站长更新网站的方式很朴素:本地改完文件,scp 传上去,或者登录服务器 git pull 一下,再手动 reload 服务。平时没问题,但只要改动涉及构建(前端打包、图片压缩、配置生成)、涉及多台机器、或者你想让「提交代码」和「上线」这两件事自动衔接,手工操作就会开始出错。
GitHub Actions 是个很好的免费选择(本站也写过它),但它托管在别人那里,私有场景、内网部署、需要复杂流水线或想完全掌控构建环境时,自建一个 Jenkins 更合适。Jenkins 是开源 CI/CD 领域的老牌工具,插件生态极其丰富,一台 2GB 内存的小机器就能跑。
本文讲清楚:为什么用 Jenkins、怎么装、怎么配一条「push 到 Gitea/GitLab → Jenkins 构建 → 部署到网站目录 → 重载服务」的完整流水线,以及几个新手必踩的坑。
Jenkins 的工作模型:控制器与构建执行
Jenkins 本身是一个常驻的 Java Web 服务(控制器/Controller),它负责调度任务、管理凭据、展示界面。真正执行构建的可以是控制器自身,也可以是外部的「代理节点(Agent)」。个人站规模小,通常让控制器自己执行就够了,但要知道这个区分,因为它关系到安全边界。
一个「流水线(Pipeline)」由若干阶段(stage)组成,比如:拉代码 → 构建 → 测试 → 部署。流水线用 Jenkinsfile 描述,可以放在代码仓库里随项目版本化,这是推荐做法;也可以在网页界面上用图形化配置,适合简单任务。
触发方式主要有三种:
- 轮询(poll SCM):Jenkins 定时去问代码仓库「有没有新提交」。实现简单但浪费资源、有延迟。
- Webhook:仓库在收到 push 时主动通知 Jenkins。实时、高效,是首选,但要求 Jenkins 能被仓库服务器访问到。
- 手动触发:适合部署、回滚这类需要人工确认的操作。
第一步:安装 Jenkins
最省事的方式是用官方 apt 仓库。先装 Java 17(Jenkins 现代版本要求 Java 11 或 17):
apt update
apt install -y fontconfig openjdk-17-jre
java -version
然后导入 Jenkins 的 GPG key、添加源、安装:
install -d -m 0755 /etc/apt/keyrings
wget -q -O /etc/apt/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc]" \
"https://pkg.jenkins.io/debian-stable binary/" \
> /etc/apt/sources.list.d/jenkins.list
apt update
apt install -y jenkins
systemctl enable --now jenkins
Jenkins 默认监听 8080 端口。千万不要直接把 8080 暴露到公网——它是一个拥有构建执行权限、能访问你的密钥和代码的服务,暴露在公网等于把服务器半条命交出去。正确做法是让它只听 127.0.0.1,通过 SSH 隧道或 Nginx 反代(带认证)访问。修改监听地址有两种方式,推荐改 systemd 环境:
# /etc/default/jenkins (老写法) 或 systemd override
systemctl edit jenkins
# 在 [Service] 段加:
# Environment="JENKINS_LISTEN_ADDRESS=127.0.0.1"
systemctl daemon-reload
systemctl restart jenkins
首次访问需要解锁,初始管理员密码在:
cat /var/lib/jenkins/secrets/initialAdminPassword
第二步:初始配置与必装插件
初始化向导会让你选择「安装推荐插件」,个人站直接选它即可。装完建议再补几个关键插件:
- Git plugin:从代码仓库拉取(推荐插件里通常已含)。
- Pipeline:流水线核心,基本必装。
- Credentials Binding:把密码、密钥安全地注入构建环境。
- SSH Agent / Publish over SSH:需要部署到远程机器时用。
- GitHub/Gitea/GitLab 插件:按你实际用的仓库平台选装,用于 webhook 集成。
在「系统管理 → 全局工具配置」里,把 JDK、Git 的路径配好,避免每次构建都手动指定。
第三步:凭据别硬编码,用 Credentials
部署脚本需要登录目标服务器、需要仓库的访问令牌,这些都是敏感信息。绝对不要把密码直接写进 Jenkinsfile 或构建脚本里。正确做法是在「系统管理 → 凭据(Credentials)」里建一条 SSH 用户名密钥或用户名密码凭据,给它一个 ID(比如 deploy-key),然后在流水线里引用它:
stage('Deploy') {
steps {
sshagent(credentials: ['deploy-key']) {
sh 'rsync -az --delete ./dist/ deploy@web1:/var/www/site/'
}
}
}
凭据在构建日志里会被自动打码。另外要理解:构建脚本本身可以执行任意命令,所以谁能改 Jenkinsfile,谁就几乎等于有服务器权限。这也是为什么 Jenkins 只能对可信的人开放提交权限。
第四步:写一条完整的 Jenkinsfile
把 Jenkinsfile 放在仓库根目录,Jenkins 任务选「Pipeline script from SCM」就能自动读取。一个典型的个人站部署流水线长这样:
pipeline {
agent any
environment {
DEPLOY_DIR = '/var/www/site'
}
options {
timestamps()
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm run lint || true'
}
}
stage('Deploy') {
steps {
sshagent(credentials: ['deploy-key']) {
sh '''
rsync -az --delete dist/ deploy@web1:${DEPLOY_DIR}/
ssh deploy@web1 "nginx -t && systemctl reload nginx"
'''
}
}
}
}
post {
failure {
echo 'Deploy failed, please check the log'
}
}
}
几个要点:disableConcurrentBuilds() 防止两次部署撞车;timestamps() 给日志加时间便于排错;post 段可以做清理或通知。注意 checkout scm 是让 Jenkins 用任务里配置的仓库地址拉代码,是流水线的标准起手式。
第五步:用 webhook 实现 push 即部署
如果你用的是 Gitea 或 GitLab 这类自建仓库,可以在仓库设置里加一个 webhook,URL 指向 Jenkins:http://jenkins主机:8080/generic-webhook-trigger/invoke 或对应平台插件提供的地址。Jenkins 任务里勾选「Build when a change is pushed」并生成一个 token 填进 webhook。
有个现实问题:如果 Jenkins 在内网、仓库在公网,webhook 打不进来。解决办法有两个:一是让仓库的 webhook 走一个能访问内网的地址(比如通过 frp/Cloudflare Tunnel 把 webhook 路径单独暴露出去,并加 token 校验);二是退而求其次用轮询,配置 pollSCM('H/5 * * * *') 每五分钟检查一次。无论哪种,webhook 端点务必带 secret token 校验,避免被人伪造请求触发构建。
第六步:几件必须做好的安全加固
- 8080 不公网:只监听 127.0.0.1,用
ssh -L 8080:127.0.0.1:8080 user@host本地转发访问管理界面。 - 启用认证并关闭匿名:在「全局安全配置」里选「Jenkins 自有用户数据库」,取消「允许匿名读取」,任何人都不能白看或白触发。
- 最小权限的部署账号:目标服务器上给 Jenkins 用的 SSH 账号只授予必要目录的写权限,最好用 sudo 白名单精确放行
systemctl reload nginx这类命令,而不是给全权 root。 - 插件及时更新:Jenkins 和插件的漏洞多半出在老版本上,关注安全公告,定期升级。
- 构建节点隔离:如果跑的构建很复杂,考虑用独立的 agent 容器执行,避免构建脚本在控制器上留下后门。
第七步:日常维护与排错
Jenkins 的日志是排错的第一入口。/var/log/jenkins/jenkins.log 记录服务端问题,单个构建的日志则在界面里查看。常见故障和处理:
- 构建卡在 Checkout:多半是仓库凭据失效或网络不通,先手动
git clone验证。 - sshagent 报权限错误:部署账号的密钥没配对,或目标机没导入主机公钥(known_hosts 未信任)。
- 磁盘被构建产物写满:Jenkins 默认保留所有历史构建,磁盘会持续膨胀。在任务配置「丢弃旧的构建」里设置只保留最近 10 次,并在系统配置里限制构建保留天数。
- 内存吃紧:Jenkins 是 Java 应用,堆内存可通过
JAVA_OPTS里的-Xmx调整,2GB 机器建议不超过 1GB。
第八步:用 Nginx 反向代理给 Jenkins 加一层门
如果你确实需要从外部访问 Jenkins 界面(比如团队成员、或者自己在外网想查看构建状态),比较稳妥的方式是用 Nginx 反代,并在反代层加上 HTTPS 和额外的基本认证。这样即便 Jenkins 监听在 127.0.0.1,也能通过 443 安全地暴露出去:
server {
listen 443 ssl http2;
server_name ci.example.com;
ssl_certificate /etc/letsencrypt/live/ci.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ci.example.com/privkey.pem;
# 再加一层 HTTP 基本认证,双保险
auth_basic "restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# webhook 可能带大 body,按需放宽
client_max_body_size 10m;
}
}
反代的关键是转发的头要齐全,尤其是 X-Forwarded-Proto,否则 Jenkins 生成的链接可能是 http 导致混合内容问题。另外记得在 Jenkins 系统配置里把「Jenkins URL」设成外部的 https 地址,这样邮件、webhook 回调里的链接才正确。.htpasswd 用 htpasswd -c /etc/nginx/.htpasswd admin 生成,密码要足够强——这层认证是暴露公网时的重要防线。
第九步:让流水线更实用——环境区分与回滚
真实项目往往有测试环境和生产环境。可以用参数化构建来区分,避免部署的时候手抖推错地方:
parameters {
choice(name: 'TARGET', choices: ['staging', 'production'], description: 'Deploy target')
}
stages {
stage('Deploy') {
steps {
script {
def host = (params.TARGET == 'production') ? 'web-prod' : 'web-stage'
sshagent(credentials: ['deploy-key']) {
sh "rsync -az --delete dist/ deploy@${host}:/var/www/site/"
sh "ssh deploy@${host} 'nginx -t && systemctl reload nginx'"
}
}
}
}
}
更稳妥的做法是在部署前先对目标目录做一次带时间戳的备份(或打快照),这样一旦新版本有问题可以快速回滚。经验上「先备份、再 rsync --delete、失败自动回滚」这三步,能挡掉绝大多数生产事故:
sh 'ssh deploy@web1 "cp -a /var/www/site /var/www/site.bak.$(date +%s)"'
sh 'rsync -az --delete dist/ deploy@web1:/var/www/site/'
sh 'ssh deploy@web1 "nginx -t && systemctl reload nginx || cp -a /var/www/site.bak.* /var/www/site/"'
注意 systemctl reload nginx 失败时才走回滚分支,写法上可以用 || 短路。回滚目录的清理也要有策略,否则时间一长磁盘全是 site.bak.xxx,可以配合定时任务只保留最近三份。
第十步:构建缓存的取舍
Node.js、PHP 项目每次构建都要重新装依赖,如果没有缓存会非常慢。Jenkins 里可以用工作区保存依赖目录,或者引入缓存策略:
stage('Build') {
steps {
// 判断依赖清单有没有变,没变就跳过 npm ci
sh '''
if [ -f node_modules/.package-lock.json ] && \
cmp -s package-lock.json node_modules/.package-lock.json; then
echo "dependencies unchanged, skip install"
else
npm ci
fi
npm run build
'''
}
}
更专业的做法是把 node_modules 或 Composer 的 vendor 目录用 Jenkins 的缓存机制或 runner 的卷保存下来。但要小心:缓存也可能带来「本地没问题、CI 拉出来就报错」的假象,所以定期做一次全新构建(rm -rf node_modules)验证依赖清单是完整的,很有必要。
小结
对个人站长来说,Jenkins 的价值是「把上线这件事变成可重复、可追溯、可回滚的流水线」。一次配置好之后,你提交代码,构建和部署自动完成,出错有日志、有历史记录,比手工 scp 可靠得多。它不像 GitHub Actions 那样零维护,但你换来的完全掌控和私有化能力,对内网项目来说往往更划算。
上手建议按最小可用推进:先装好 Jenkins 并锁死 8080,再配一条最简单的「拉代码 → rsync 部署」流水线跑通,之后逐步加构建、测试、通知等阶段。安全上别偷懒,8080 不公网、凭据用 Credentials、部署账号最小权限,这三条守住了,自建 CI/CD 就不会成为新的风险点。