代码在自己的电脑上能跑,换到服务器却因为依赖、环境变量或工作目录不同而失败。把测试交给一台固定的构建机器,可以早一点发现这些差异。Woodpecker CI 接收 Git 仓库事件,把仓库里的流水线配置交给 agent 执行,结果回到网页里,每次提交都能留下检查记录。
这篇先做一个只运行测试的实例,不急着让它自动发布生产网站。服务器和 agent 可以放在同一台专门的构建 VPS 上;它们不要和正式业务数据库、私人密码库放在一起。本文使用 Docker 后端,agent 会挂载宿主机 Docker socket,这相当于给构建系统很强的宿主机控制能力,不能交给陌生人的仓库任意执行。
两个容器各自负责什么
Woodpecker server 提供网页、处理 GitHub 登录和 Webhook,保存用户、仓库与任务信息。agent 连接 server 的内部服务,领取任务,再启动流水线需要的容器。浏览器访问的是 server 的 8000 端口,agent 通信使用内部 9000 端口;后者在本例中不发布到公网。
要准备一个 GitHub 账号、一个你有管理权限的测试仓库、一个解析到构建 VPS 的域名,以及 Docker Engine、Compose 插件。Caddy 运行在宿主机,为 ci.example.com 提供 HTTPS。编译大型项目的资源需求和运行一个网页不同,先从一个小测试仓库测量 CPU、内存和磁盘使用,再决定是否提高并发。
构建容器有时会下载依赖或镜像,构建机器的网络会影响结果。锁文件、固定的基础镜像和明确的测试命令能减少差异,但不能消除上游下载失败。构建失败时需要看第一条有效错误,不要看到最后的非零退出码就判断程序出了问题。
建立 GitHub OAuth 应用
在 GitHub 开发者设置中创建 OAuth App,首页地址填 https://ci.example.com,回调地址填 https://ci.example.com/authorize。这里使用 OAuth App,不能随手换成 GitHub App。创建后记录 Client ID,并生成 Client Secret,后者只写在服务器配置里,不提交到仓库,也不截进教程图片。
接下来在 VPS 上创建目录。当前用户需要有 Docker 权限,没有时通过管理员安排或使用 sudo。
mkdir -p /opt/woodpecker/server-data /opt/woodpecker/agent-config
cd /opt/woodpecker
umask 077
openssl rand -hex 32 > agent-secret.txt
创建 .env,按下面的字段填写。WOODPECKER_ADMIN 是 GitHub 登录名,不是显示昵称或邮箱。WOODPECKER_AGENT_SECRET 填刚生成文件里的随机值;它不是 GitHub Client Secret。.env 保存完成后限制为仅所有者可读写。
WOODPECKER_HOST=https://ci.example.com
WOODPECKER_ADMIN=your-github-login
WOODPECKER_GITHUB_CLIENT=your-oauth-client-id
WOODPECKER_GITHUB_SECRET=your-oauth-client-secret
WOODPECKER_AGENT_SECRET=your-generated-agent-secret
chmod 600 .env agent-secret.txt
先保留关闭注册,只允许配置的管理员进入。仓库同步和执行权限仍与 GitHub 账号权限相关,管理员能看到多少仓库,应在接入时检查。小团队后来需要添加成员,再按当前版本的用户管理方式操作,没必要为了一个账号把注册长期向所有 GitHub 用户开放。
Compose 里保留必要的边界
保存为 compose.yaml。两个组件使用同一主版本的镜像标签,首次拉取后记录对应摘要,长期运行固定到验证过的版本或摘要。浮动的 v3 标签会跟随该主版本的新发布变化,本例不会安排自动更新。
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:v3
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000"
volumes:
- ./server-data:/var/lib/woodpecker
environment:
WOODPECKER_OPEN: "false"
WOODPECKER_ADMIN: ${WOODPECKER_ADMIN:?set admin}
WOODPECKER_HOST: ${WOODPECKER_HOST:?set host}
WOODPECKER_GITHUB: "true"
WOODPECKER_GITHUB_CLIENT: ${WOODPECKER_GITHUB_CLIENT:?set client}
WOODPECKER_GITHUB_SECRET: ${WOODPECKER_GITHUB_SECRET:?set secret}
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:?set agent secret}
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:v3
command: agent
restart: unless-stopped
depends_on:
- woodpecker-server
volumes:
- ./agent-config:/etc/woodpecker
- /var/run/docker.sock:/var/run/docker.sock
environment:
WOODPECKER_SERVER: woodpecker-server:9000
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:?set agent secret}
WOODPECKER_MAX_WORKFLOWS: "1"
默认 SQLite 保存在 server 数据目录中。agent 配置也要保留,以免每次重建都丢失本地身份信息。depends_on 安排启动顺序,不代表 server 此时已经完成初始化;启动过程中短暂重连和一直无法连接是两种情况,应结合日志判断。
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 woodpecker-server woodpecker-agent
docker compose config 展开后会含有机密,所以这里只运行 --quiet 检查。排障时不要把完整展开输出贴到公开问题区。查看 agent 日志,确认它已连接 server;如果一直认证失败,对比两边的 agent secret 来源,不要换掉 GitHub Secret 来碰运气。
把域名接通,再接仓库
宿主机 Caddy 添加以下站点段,校验配置后重载。公网要能访问 80、443,使证书签发和 GitHub Webhook 正常到达。代理若也是容器,应使用共享网络上的 server 地址,不能使用这里的宿主机回环地址。
ci.example.com {
reverse_proxy 127.0.0.1:8000
}
用 GitHub 登录,核对授权范围。回调报错时,检查 .env 的域名、OAuth App 的首页和回调地址是否一致,特别是 HTTPS、子域名以及 /authorize。更换域名需要同时更改这几处,只有 DNS 改对还不够。
在网页里添加测试仓库,让 Woodpecker 建立对应的事件连接。仓库列表空白可能与账号权限、组织 OAuth 限制或同步范围有关。仓库已添加但没有任务,则检查 GitHub Webhook 的投递记录,看看是连接失败、响应错误,还是根本没有触发目标事件。
第一条流水线只回答“测试是否通过”
假定测试仓库已经有 package-lock.json,且 package.json 中的 test 命令会实际执行测试。创建 .woodpecker.yaml,先不要放部署私钥或生产数据库密码。Node 主版本应与项目声明的运行版本一致,这里以 Node 22 的 Alpine 镜像为例。
steps:
- name: test
image: node:22-alpine
commands:
- npm ci
- npm test
when:
- event: push
branch: main
将文件提交到测试仓库的 main 分支。配置把任务限制在该分支的 push 事件,其他分支或 pull request 不会按此示例执行。需要检查合并请求时,再增加对应事件规则,先明确是否允许来自 fork 的代码,以及那些任务能不能访问 secrets。
验收时故意让一条测试失败,确认页面显示失败和对应日志,再修正提交,确认下一次通过。若 npm test 只是打印一句提示并退出成功,CI 的绿色状态没有测试意义。没有锁文件的项目,也不能照搬 npm ci;先按项目包管理器准备锁文件。
不要在最初的流水线里加入“一通过就 SSH 登录生产服务器”。等测试可靠以后,再增加构建成品和发布流程,把发布限定到受保护分支或人工批准的事件。测试凭据、制品仓库凭据和部署凭据分别设置,能减少一次配置泄漏牵连所有环节。
防止构建磁盘悄悄被填满
几次任务后检查 Docker 占用和 VPS 剩余空间。依赖缓存、镜像层与构建输出会累积,磁盘耗尽可能先表现成下载失败或数据库无法写入。先确认哪些缓存可以删除,再安排清理,不要用删除所有卷的命令作为日常维护手段。
任务排队但不启动,先检查 agent 是否在线和并发是否占满。拉取基础镜像失败,检查 registry 访问、名称和标签;测试阶段报错则读项目输出。找到失败发生的阶段,再决定修网络、改流水线还是改代码。
构建机器只接入受信任的仓库,限制能修改流水线的人。socket 挂载即使写成只读,也不等于 Docker API 只读,不能靠这一点把 agent 变成安全沙箱。要执行外部贡献者提交的代码,应使用更强的隔离设计和不含生产机密的独立执行环境。
留下能重建 CI 的材料
备份需要保存 server 数据、agent 配置、Compose 和 .env。先停下入口触发或安排维护窗口,等待正在运行的任务结束,再停止组件取得一致副本。备份中有 OAuth 与 agent 机密,需要访问限制和离机存储。
cd /opt/woodpecker
mkdir -p /opt/backups
docker compose stop woodpecker-agent woodpecker-server
tar -czf "/opt/backups/woodpecker-$(date +%F-%H%M).tgz" compose.yaml .env server-data agent-config
docker compose start woodpecker-server woodpecker-agent
恢复演练时先不要把旧域名或 GitHub Webhook 指向测试实例,也不要让测试 agent 执行正式任务。检查历史仓库记录、登录配置和数据完整性,再决定切换入口。正式迁移需要重新核对 OAuth 回调和 Webhook 地址,单纯恢复数据库并不会让新的公网地址自动接上旧事件。
运行稳定后,最有用的变化是每次提交都能看到同样的测试过程。有人问“这次改动有没有通过检查”,可以直接打开对应任务,而不是靠提交者回忆自己昨天在电脑上执行过哪条命令。