为什么需要自托管 CI/CD
代码写完只是第一步。测试、构建、部署——这些重复性劳动不该手工执行。CI/CD 解决的核心问题:
- 自动化测试:每次推送代码自动跑测试,避免”在我机器上能跑”的尴尬
- 自动构建:代码合并后自动构建 Docker 镜像、静态资源
- 自动部署:通过 main 分支更新触发自动部署到服务器
- 完全自控:代码和数据都在自己设备上,不依赖 GitHub Actions 等第三方服务
Woodpecker CI 是 Drone CI 的开源 fork,专为 Gitea/Forgejo 等轻量级 Git 平台设计。自 2022 年从 Drone 分叉以来,Woodpecker 已成为自托管 CI/CD 领域的首选方案之一——轻量、配置简单、资源占用低,非常适合内网环境。
Woodpecker 最新稳定版为 v3.14.1(2026-05-12 发布)。请查阅 GitHub Releases 确认当前版本号。
Woodpecker CI 是什么
Woodpecker CI 是 Go 语言编写的轻量级 CI/CD 系统,核心特点:
| 特性 | 说明 |
|---|---|
| 原生 Gitea/Forgejo 集成 | OAuth2 登录,自动 Webhook,无缝对接 |
| Server + Agent 架构 | Server 负责 Web UI 和调度,Agent 负责执行 Pipeline |
| 容器化执行 | 每个构建步骤在独立 Docker 容器中运行,环境隔离 |
| YAML 配置即代码 | .woodpecker.yml 文件定义 Pipeline,纳入版本管理 |
| 插件生态 | 复用 Drone 兼容插件,支持 Docker 构建、通知、部署等 |
| 资源占用低 | Server 内存 ~50 MB,Agent 内存 ~30 MB |
| CLI 工具 | woodpecker-cli 支持本地调试 Pipeline(v3.0+ cli exec) |
与同类工具对比
| 特性 | Woodpecker CI | Drone CI | Jenkins | Gitea Actions |
|---|---|---|---|---|
| 开源协议 | Apache 2.0 | 部分功能闭源 | MIT | MIT |
| 资源占用 | 低 | 低 | 高 | 中 |
| Gitea 集成 | 原生 | 原生 | 需插件 | 内置 |
| 容器隔离 | 是 | 是 | 否(进程级) | 是(DinD) |
| 配置复杂度 | 低 | 低 | 高 | 低 |
| 插件生态 | 兼容 Drone 插件 | 丰富 | 极丰富 | 有限 |
| 维护状态 | 活跃开源社区 | 企业化运营 | 老牌项目 | Gitea 官方 |
对于自托管 Gitea 用户,Woodpecker CI 和 Gitea Actions 是两个最直接的选择。Gitea Actions 胜在零额外部署(Gitea 内置 Runner),但 Woodpecker CI 提供更成熟的 Pipeline 语法、Drone 插件兼容和更好的调度控制。
系统要求
| 项目 | 最低要求 | 推荐配置 |
|---|---|---|
| CPU | 1 核 | 2 核+ |
| 内存 | 512 MB(Server + Agent) | 1 GB+ |
| 磁盘 | 2 GB(SQLite 数据库 + 构建缓存) | 10 GB+ |
| Docker | ≥ 20.10 | 最新稳定版 |
| Gitea | ≥ 1.18(OAuth2 支持) | 最新稳定版(1.26.2,2026-05-20 发布) |
Server 本身极为轻量(~50 MB 内存),资源消耗大头在 Agent 执行的构建任务。如果同一台机器上跑多个并发构建(如多平台 Docker 镜像),建议 4 GB+ 内存和 4 核以上 CPU。
生产环境数据库建议
Woodpecker Server 默认使用 SQLite 存储元数据(Pipeline 历史、仓库列表、用户信息)。SQLite 适合个人或小团队使用(< 50 个仓库、Pipeline 历史较少)。当仓库数量增多或 Pipeline 历史累积后,建议切换到 PostgreSQL:
| 场景 | 推荐数据库 |
|---|---|
| 个人/小团队(< 50 仓库) | SQLite(默认,零配置) |
| 团队/生产环境 | PostgreSQL 16+ |
使用 PostgreSQL 只需在 Server 容器中配置环境变量:
environment: - WOODPECKER_DATABASE_DRIVER=postgres - WOODPECKER_DATABASE_DATASOURCE=postgres://user:password@postgres:5432/woodpecker?sslmode=disableSQLite 和 PostgreSQL 之间不能直接迁移数据。建议一开始就评估需求,避免后期迁移。
架构概览
graph LR User[开发者 Push 代码] --> Gitea[Gitea 代码仓库] Gitea -->|Webhook| WoodpeckerServer[Woodpecker Server] WoodpeckerServer -->|gRPC :9000| WoodpeckerAgent[Woodpecker Agent] WoodpeckerAgent -->|Docker API| DockerContainers[构建容器] WoodpeckerAgent -->|git clone| Gitea关键通信路径:
- 用户 → Woodpecker Server (:8000):Web UI 和 API
- Gitea → Woodpecker Server (:8000):Webhook 事件推送
- Woodpecker Server → Agent (:9000):gRPC,任务下发和日志回传
- Agent → Gitea:git clone 拉取代码,需在同一 Docker 网络内
Agent 拉取代码依赖容器间网络互通。构建容器内必须能解析 Gitea 主机名——可通过加入同一 Docker 网络、使用
docker0网关 IP 或host网络模式实现。推荐做法是将 Gitea 和 Agent 放入同一自定义网络。
Docker Compose 部署
前提条件
- Gitea 已部署运行(假设地址为
http://192.168.1.100:3000,服务名为gitea) - Docker 和 Docker Compose 已安装
- Gitea 容器所在 Docker 网络已知(如
gitea_default或自定义网络)
Step 1 — 创建 Gitea OAuth2 应用
在 Gitea 中创建 OAuth2 应用。有两种方式,任选其一:
- 用户自建(推荐,仅授权个人仓库):Gitea → 头像 → 设置 → 应用 → 创建新的 OAuth2 应用程序
- 管理员全局创建(授权所有用户仓库):Gitea → 站点管理 → 应用 → 创建新的 OAuth2 应用程序
| 字段 | 值 |
|---|---|
| 应用名称 | Woodpecker CI |
| 重定向 URI | http://192.168.1.100:8000/authorize |
回调 URI 的协议和主机名必须与
WOODPECKER_HOST完全一致。如果通过 Nginx 反代使用 HTTPS,URI 也应改为https://your-domain/authorize。
- 创建后获取 客户端 ID 和 客户端密钥,后续配置需要。
Step 2 — 配置 Gitea 允许本地 Webhook
如果 Gitea 和 Woodpecker 在同一台服务器上(默认 Docker 网络),Gitea 默认禁止向本地地址发送 Webhook。编辑 Gitea 的 app.ini:
[webhook]ALLOWED_HOST_LIST = external,loopbackStep 3 — 生成 Agent 共享密钥
openssl rand -hex 32复制生成的字符串,Server 和 Agent 两边需一致。
Step 4 — Docker Compose
创建目录和配置文件:
mkdir -p /opt/woodpecker && cd /opt/woodpeckerservices: woodpecker-server: image: woodpeckerci/woodpecker-server:v3 container_name: woodpecker-server restart: unless-stopped ports: - "8000:8000" # Web UI + API - "9000:9000" # gRPC (Agent 连接) volumes: - ./woodpecker-data:/var/lib/woodpecker/ environment: - WOODPECKER_OPEN=true - WOODPECKER_HOST=http://192.168.1.100:8000 - WOODPECKER_GITEA=true - WOODPECKER_GITEA_URL=http://192.168.1.100:3000 - WOODPECKER_GITEA_CLIENT=4f8a7b2c-xxxx-xxxx-xxxx-xxxxxxxxxxxx - WOODPECKER_GITEA_SECRET=gto_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - WOODPECKER_AGENT_SECRET=<上一步生成的共享密钥> - WOODPECKER_ADMIN=your-gitea-username networks: - cicd
woodpecker-agent: image: woodpeckerci/woodpecker-agent:v3 container_name: woodpecker-agent restart: unless-stopped command: agent depends_on: - woodpecker-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - WOODPECKER_SERVER=woodpecker-server:9000 - WOODPECKER_AGENT_SECRET=<同上共享密钥> # - WOODPECKER_MAX_WORKFLOWS=2 # 默认 1,根据 CPU/内存调整 - WOODPECKER_BACKEND_DOCKER_NETWORK=cicd networks: - cicd
networks: cicd: name: cicd external: true # 需预先创建:docker network create cicd
volumes: woodpecker-data:使用显式自定义网络
cicd,确保 Gitea 也加入同一网络:docker network connect cicd gitea。构建容器内必须能解析 Gitea 主机名(容器名或 LAN IP),否则git clone会 DNS 解析失败。如有多个网络,用逗号分隔WOODPECKER_BACKEND_DOCKER_NETWORK。
Step 5 — 启动并验证
docker compose up -d查看日志确认无报错:
docker compose logs woodpecker-serverdocker compose logs woodpecker-agent访问 http://192.168.1.100:8000,点击 Login,会自动跳转到 Gitea 的 OAuth 授权页面。授权后回到 Woodpecker 仪表盘。
首次登录后
- 左侧 Repositories 列表为空白,手动点 Add repository 搜索并启用仓库
- 启用的仓库会自动在 Gitea 中创建 Webhook
- 之后 Push 代码即自动触发 Pipeline
Agent 注册机制(v3+)
Woodpecker v3 支持用户级和组织级 Agent 注册,作为安全增强选项:
- 全局 Agent(默认):Server 级别共享,任意仓库的 Pipeline 均可使用
- 用户级 Agent:仅处理特定用户的仓库任务
- 组织级 Agent:仅处理特定 Gitea 组织的仓库任务,适合多团队隔离
启用组织级 Agent 注册,在 Server 环境变量中添加:
environment: - WOODPECKER_AGENT_REGISTRATION_ENABLED=true然后在 Agent 端指定归属:
environment: - WOODPECKER_AGENT_LABELS=org=my-team用户/组织级 Agent 需要用户在 Woodpecker UI 中手动创建 Agent 令牌并填入 Agent 配置。全局 Agent 只需共享密钥即可自动注册。
Rootless 镜像支持
Woodpecker 官方提供 -rootless 后缀的镜像变体,以非 root 用户运行,适合安全敏感环境:
services: woodpecker-agent: image: woodpeckerci/woodpecker-agent:v3-rootless # ...Rootless Agent 的 Docker socket 访问需配合 rootless Docker 或 docker-socket-proxy 使用。
Pipeline 语法基础
Pipeline 定义在仓库根目录的 .woodpecker.yml(单位置)或 .woodpecker/ 目录下(多工作流拆分)。
最简示例
steps: - name: test image: node:22-alpine commands: - npm install - npm test每个 step 在独立容器中运行,/woodpecker 路径自动挂载为工作目录,步骤间共享。
触发条件:when
控制哪些事件触发 Pipeline:
when: - event: push branch: main - event: pull_request - event: tag - event: cron cron: nightly| 事件 | 触发时机 |
|---|---|
push | 代码推送到仓库 |
pull_request | 创建或更新 PR |
tag | 创建 Git 标签 |
cron | 定时器触发(在 Woodpecker UI 中配置) |
manual | 在 UI 中手动点击运行 |
deployment | 部署事件触发 |
多工作流拆分
大型项目建议拆分工作流文件,放在 .woodpecker/ 目录:
.woodpecker/├── lint.yaml # 代码检查├── test.yaml # 单元测试├── build.yaml # 构建 Docker 镜像└── deploy.yaml # 部署到服务器通过 depends_on 控制执行依赖:
depends_on: - lint - test - build
steps: - name: deploy image: debian:stable-slim commands: - echo "deploying..."矩阵构建
同一 Pipeline 在不同环境下并行执行:
matrix: GO_VERSION: - 1.22 - 1.23 PLATFORM: - linux/amd64 - linux/arm64
steps: - name: build image: golang:${GO_VERSION} commands: - go build -o app . environment: - GOOS=${PLATFORM%%/*} - GOARCH=${PLATFORM##*/}依赖服务(services)
当测试需要数据库、缓存等外部服务时,用 services: 声明随 Pipeline 生命周期启动/销毁的辅助容器:
services: postgres: image: postgres:16-alpine environment: - POSTGRES_USER=test - POSTGRES_PASSWORD=test - POSTGRES_DB=testdb
redis: image: redis:7-alpine
steps: - name: test image: node:22-alpine commands: - npm ci - npm test environment: - DATABASE_URL=postgres://test:test@postgres:5432/testdb - REDIS_URL=redis://redis:6379
services:中容器的主机名即服务名(如postgres、redis),可直接在步骤的环境变量中引用。Pipeline 结束后这些容器自动销毁。
密钥管理
Woodpecker v3 使用 from_secret 语法(旧版 secrets: 关键字已移除):
添加密钥
在 Woodpecker UI → 仓库 → Settings → Secrets 中添加密钥(如 docker_username、docker_password)。
在 Pipeline 中使用
steps: - name: deploy image: woodpeckerci/plugin-docker-buildx settings: repo: registry.example.com/my-app registry: registry.example.com tags: latest username: from_secret: docker_username password: from_secret: docker_password环境变量方式:
steps: - name: publish image: node:22-alpine commands: - echo "//registry.npmjs.org/:_authToken=$${NPM_TOKEN}" > .npmrc - npm publish environment: NPM_TOKEN: from_secret: npm_token命令中使用
$$(双美元符)转义变量,防止 Woodpecker 前端预处理时替换。单$会在 YAML 解析阶段被替换为空(未定义的 YAML 变量),导致最终命令中变量消失。
密钥安全要点
- PR 保护:默认 PR 事件不注入密钥,防止恶意 PR 泄露凭据
- 镜像过滤:可限制密钥仅特定插件可用,防止泄露给不受信任的容器
- 全局密钥:在 Woodpecker Server 级别添加的密钥对所有仓库可见,慎用
实战示例
示例 1:前端项目构建部署
场景:构建静态站点,通过 rsync 同步到服务器。
steps: - name: build image: node:22-alpine commands: - npm ci - npm run build
- name: deploy image: debian:stable-slim commands: - apt-get update && apt-get install -y rsync openssh-client - mkdir -p ~/.ssh && echo "$${SSH_KEY}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa - rsync -avz -e "ssh -o StrictHostKeyChecking=no" dist/ user@192.168.1.10:/var/www/html/ environment: SSH_KEY: from_secret: deploy_ssh_key when: event: push branch: main示例 2:Go 项目测试 + 构建 + Docker 镜像
场景:Go 后端项目,自动测试、交叉编译、构建多架构 Docker 镜像。
matrix: GO_VERSION: - 1.22 - 1.23
steps: - name: test image: golang:${GO_VERSION} commands: - go vet ./... - go test -race -coverprofile=coverage.out ./... - go tool cover -func=coverage.out
- name: build image: golang:${GO_VERSION} commands: - CGO_ENABLED=0 go build -ldflags="-s -w" -o app .
- name: publish-image image: woodpeckerci/plugin-docker-buildx settings: repo: registry.example.com/myapp registry: registry.example.com dockerfile: Dockerfile platforms: linux/amd64,linux/arm64 tags: latest auto_tag: true username: from_secret: registry_user password: from_secret: registry_pass when: event: push branch: main
auto_tag: true根据 Git 标签自动生成 semver 标签。推送v1.2.3标签时,同时打上v1.2.3、v1.2、v1三个标签。
示例 3:Cron 定时任务
场景:每天凌晨 3 点备份数据库。
在 Woodpecker UI → 仓库 → Settings → Cron Jobs 中添加名为 daily-backup 的定时器,Cron 表达式 0 3 * * *。
when: - event: cron cron: daily-backup
steps: - name: backup-db image: postgres:16-alpine commands: - pg_dump -h $$DB_HOST -U $$DB_USER $$DB_NAME | gzip > backup-$(date +%Y%m%d).sql.gz environment: DB_HOST: from_secret: db_host DB_USER: from_secret: db_user DB_NAME: from_secret: db_name PGPASSWORD: from_secret: db_passwordNginx 反向代理
生产环境建议将 Woodpecker 放在 Nginx 反代后面,启用 HTTPS。
Web UI 反代(端口 8000)
server { listen 443 ssl; server_name ci.example.com;
ssl_certificate /etc/ssl/certs/ci.example.com.pem; ssl_certificate_key /etc/ssl/private/ci.example.com.key;
location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1;
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;
# WebSocket 支持(日志实时推流需要) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_read_timeout 300s; proxy_send_timeout 300s; }}gRPC 反代(端口 9000,Agent 与 Server 分离时)
当 Agent 部署在另一台机器、需要通过公网或跨网段连接 Server 时,需反代 gRPC 端口。gRPC 要求 HTTP/2 且必须端到端 TLS:
server { listen 443 ssl http2; server_name ci-grpc.example.com;
ssl_certificate /etc/ssl/certs/ci-grpc.example.com.pem; ssl_certificate_key /etc/ssl/private/ci-grpc.example.com.key;
location / { grpc_pass grpc://127.0.0.1:9000; }}Agent 端对应配置:
environment: - WOODPECKER_SERVER=ci-grpc.example.com:443 - WOODPECKER_GRPC_SECURE=true如果 Agent 和 Server 在同一台机器或同一 Docker 网络内,无需反代 9000 端口,Agent 直接连
woodpecker-server:9000即可。
本地调试 Pipeline(cli exec)
Woodpecker v3.0+ 支持 cli exec——无需推送到仓库也能在本地跑 Pipeline,调试效率大幅提升。
安装 CLI
# 从 GitHub Releases 下载curl -L https://github.com/woodpecker-ci/woodpecker/releases/download/v3.x.x/woodpecker-cli_linux_amd64 -o /usr/local/bin/woodpecker-clichmod +x /usr/local/bin/woodpecker-cli使用方法
- 在 Woodpecker UI 中,进入某个 Pipeline 的运行详情
- 点击 Download metadata 下载 JSON 元数据文件
- 创建密钥文件(不要直接在命令行传密钥,避免泄露到 shell 历史):
# secrets.env(不要提交到 Git)docker_username=admindocker_password=xxx- 本地执行:
woodpecker-cli exec \ --local \ --metadata-file=metadata.json \ --secrets-file=secrets.env密钥不包含在元数据文件中,需通过
--secrets-file(推荐)或--secrets手动传入。--secrets="key=val"方式会将密钥暴露在 shell 历史和ps输出中,仅用于临时调试。
数据备份
Woodpecker Server 默认使用 SQLite 存储元数据(仓库列表、Pipeline 历史、用户信息),数据库位于 woodpecker-data/woodpecker.db。
# 在线备份docker exec woodpecker-server sqlite3 /var/lib/woodpecker/woodpecker.db ".dump" | gzip > \ /backup/woodpecker/woodpecker-$(date +%Y%m%d-%H%M).sql.gzCron 定时备份脚本:
#!/bin/bashBACKUP_DIR="/backup/woodpecker"RETENTION_DAYS=14mkdir -p "$BACKUP_DIR"
docker exec woodpecker-server sqlite3 /var/lib/woodpecker/woodpecker.db ".dump" | gzip > \ "$BACKUP_DIR/woodpecker-$(date +%Y%m%d-%H%M).sql.gz"
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -deleteecho "Backup completed: $(date)"0 5 * * * /opt/scripts/backup-woodpecker.sh >> /var/log/woodpecker-backup.log 2>&1Woodpecker 本身的数据库损坏不影响 Gitea 代码,最坏情况重新部署并激活仓库即可。但 Pipeline 历史记录和密钥会丢失,建议定期备份。
常见问题
Q:登录后仓库列表为空
检查 Gitea OAuth2 回调地址是否与 WOODPECKER_HOST 完全一致(协议、域名、端口均需匹配)。点击 Add repository 搜索,确认 Gitea 用户有对应仓库的读写权限。
Q:Pipeline 不触发,Webhook 未送达
排查步骤:
- Gitea 中检查仓库 → 设置 → Webhooks,确认 Woodpecker Webhook 存在且状态正常
- 查看 Webhook 最近投递记录,错误
dial tcp 192.168.1.100:8000: connect: connection refused说明 Gitea 无法访问 Woodpecker 的 8000 端口 - Docker 容器间通信:确认 Gitea 和 Woodpecker 在同一 Docker 网络,或使用 host 网络模式
- 检查 Gitea
app.ini是否配置了ALLOWED_HOST_LIST = external,loopback
Q:Webhook 投递成功(HTTP 200),但 Pipeline 不触发
Webhook 正常送达但 Woodpecker 没有创建 Pipeline,常见原因:
- 事件过滤不匹配:检查
.woodpecker.yml中的when:条件。例如配置了branch: main但推送到了dev分支,或配置了event: push但当前操作为创建 Tag - 仓库未在 Woodpecker 中激活:前往 Woodpecker UI → Repositories,确认仓库状态为 “Active”。如果仓库被停用或未激活,Webhook 会被忽略
- 配置文件路径错误:确认
.woodpecker.yml在仓库根目录,或工作流文件在.woodpecker/目录下。文件名必须以.yml或.yaml结尾 - YAML 语法错误:Pipeline 配置文件存在 YAML 语法错误时会被静默跳过。用
yamllint或在 Woodpecker UI 中手动触发一次 Pipeline 以查看错误消息
排查时查看 Woodpecker Server 日志:
docker compose logs woodpecker-server | grep -i "pipeline\|error\|webhook"Q:构建容器内 git clone 失败,DNS 解析不到 Gitea
Agent 容器需要能解析 Gitea 的主机名。确认:
WOODPECKER_BACKEND_DOCKER_NETWORK包含了 Gitea 所在的 Docker 网络- 构建容器内使用与 Agent 相同的 Docker 网络,可解析 Gitea 容器名
# 查看 Gitea 所在网络docker inspect gitea | grep -A 5 Networks
# 确认 Agent 加入同网络docker inspect woodpecker-agent | grep -A 5 NetworksQ:Docker 镜像构建失败,提示权限不足
Docker 镜像构建需要特权模式。在 Woodpecker Server 添加环境变量:
environment: - WOODPECKER_PLUGINS_PRIVILEGED=woodpeckerci/plugin-docker-buildxQ:from_secret 报错 “secret not found”
- 确认密钥名称拼写一致(密钥名不区分大小写)
- 确认密钥已在对应仓库的 Settings → Secrets 中添加
- 确认 Pipeline 事件不是
pull_request(PR 默认不注入密钥)
Q:环境变量在命令中为空
命令中的 $VARIABLE 在 YAML 解析阶段被视为 YAML 变量,如果未定义则为空字符串。使用 $${VARIABLE} 让 Woodpecker 将 $ 字面传给 Shell。
# 错误commands: - echo $MY_VAR # YAML 解析为 echo (空字符串)
# 正确commands: - echo $${MY_VAR} # YAML 解析为 echo $MY_VAR,Shell 再展开