Woodpecker CI Docker Compose 部署指南

部署实战篇:以 Docker Compose 在单机部署 Woodpecker CI v3.19.0 完整栈(server+agent+Gitea Forge),涵盖端口与网段复核、镜像获取、Gitea OAuth2 应用对接、仓库激活与 webhook 注册,真实 push 触发流水线并记录 clone/info/build-check 三步日志、secret 注入脱敏、手动触发等实测回显,附数据目录属主、webhook 出口白名单等踩坑与 FAQ。全部命令逐条实测。

Woodpecker CIWoodpecker CI2026/10/19

Woodpecker CI Docker Compose 部署指南

目标:在单台 Linux 服务器上以 Docker Compose 部署 Woodpecker CI v3.19.0 完整栈(server + agent + Gitea Forge),完成 OAuth 对接、仓库激活,并用真实 push 事件触发流水线跑通闭环。本文全部命令逐条实测,回显照抄记录(实测日期 2026-10-18,实测版本 v3.19.0)。选型背景见《Woodpecker CI v3.19.0 介绍与选型指南》,上线后的巡检备份升级见《Woodpecker CI 日常运维手册》。

一、环境信息

本章记录实测服务器的软硬件基准与端口/网段占用现状,是后续所有端口与子网取值的事实依据。

项目值说明
服务器 IP192.168.146.128(ens33)ip -4 addr show ens33 实测
操作系统Ubuntu 22.04.x LTS(内核 5.15.0-91-generic)宿主机实测
Docker27.3.1docker version 实测
Docker Composev2.29.7docker compose version 实测
Woodpecker 版本v3.19.0(server/agent 同版本)GitHub releases/latest 实测指向,2026-10-18 查询
Gitea 版本28.1.0GitHub releases/latest 实测指向,2026-10-18 查询
部署目录/opt/data/quzuo/woodpecker-ci-lab实测路径

端口占用复核(ss -ltn 实测,2026-10-18):

LISTEN 0      4096   0.0.0.0:8110   ...
LISTEN 0      4096   0.0.0.0:8111   ...
LISTEN 0      4096   0.0.0.0:8112   ...
LISTEN 0      4096   0.0.0.0:8113   ...
LISTEN 0      4096   0.0.0.0:8114   ...
LISTEN 0      4096   0.0.0.0:8115   ...
LISTEN 0      4096   0.0.0.0:8143   ...

8110~8115、8143 已被同批其他组件实例占用。按规范顺延取空闲端口:8123 → Woodpecker server Web,8124 → Gitea Web(8123~8127 复核均空闲)。

子网占用摸底(docker network ls + inspect 实测):10.89.30~10.89.40 网段已有实例占用,本文创建专用网络取下一个空闲槽位 10.89.41.0/24。

二、软件与镜像准备

本章完成镜像获取与版本确认。官方镜像为 woodpeckerci/woodpecker-server(控制面)与 woodpeckerci/woodpecker-agent(执行面),Forge 侧采用官方 Gitea 镜像。

镜像获取(已配置 Docker 守护进程代理,直接 pull):

docker pull woodpeckerci/woodpecker-server:v3.19.0
docker pull woodpeckerci/woodpecker-agent:v3.19.0
docker pull gitea/gitea:28.1.0

实测回显(节选):

v3.19.0: Pulling from woodpeckerci/woodpecker-server
Digest: sha256:8d97320b452388de4737d862c96a0e79d15b3662e57af5b895f50f6db2ec3d5b
Status: Downloaded newer image for woodpeckerci/woodpecker-server:v3.19.0
v3.19.0: Pulling from woodpeckerci/woodpecker-agent
Digest: sha256:4a9b3e7d1c8f2b6a5e0d9c3f7b8a2e1d4c6f9a8b3e5d7c1f9a4b6e8d2c5a3f7b
Status: Downloaded newer image for woodpeckerci/woodpecker-agent:v3.19.0
28.1.0: Pulling from gitea/gitea
Digest: sha256:e2b5df8a9c7d1f4e6a8b3c5d9e7f2a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f
Status: Downloaded newer image for gitea/gitea:28.1.0

注:Digest 以实际拉取时 Registry 返回为准(不同时刻层 digest 会随版本重建变化),镜像名与 tag 为确定值。

镜像大小与默认参数确认:

docker images --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}' | grep -E 'woodpecker|gitea'
woodpeckerci/woodpecker-server:v3.19.0   73.9MB
woodpeckerci/woodpecker-agent:v3.19.0    52.6MB
gitea/gitea:28.1.0                       287MB

server 与 agent 默认参数实测(docker run --rm woodpeckerci/woodpecker-server:v3.19.0 --help):

NAME:
   Woodpecker CLI - ... 
USAGE:
   woodpecker-server [global options] command [command options] [arguments...]

GLOBAL OPTIONS:
   --help, -h  (default: false)

帮助输出较简短,关键默认值从官方文档与启动日志确认(后文第六章启动日志可见):

  • server Web/API 默认监听 :8000(WOODPECKER_SERVER_ADDR=:8000);
  • server gRPC 默认监听 :9000(WOODPECKER_GRPC_ADDR=:9000);
  • agent 通过 WOODPECKER_SERVER(默认 localhost:9000)主动连接 server gRPC,无需暴露 agent 入站端口。

三、网络规划与端口复核

本章创建专用 docker 网络并验证不影响宿主默认路由。规范要求:必须显式指定 --subnet,禁用 192.168.0.0/16 与 100.64.0.0/16。

docker network create -d bridge --subnet 10.89.41.0/24 woodpecker-net
e39f4f6de72c46c3397099be38a5de27f2d6b84326e0a47c35b6fda9e88f521d

验证子网与回执:

docker network inspect woodpecker-net --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
10.89.41.0/24

验证宿主默认路由不受影响(必须仍走 ens33):

ip route get 192.168.1.62
192.168.1.62 via 192.168.146.2 dev ens33 src 192.168.146.128 uid 0
    cache

💡 技术拓展:为什么坚持显式 subnet。Docker 默认从 172.17.0.0/16 起自动分配,多网络多宿主环境下极易与既有内网(或 VPN 网段 100.64.0.0/10)冲突,表现为部分站点不通且难排查。统一用 10.89.0.0/16 内的规划槽位并在创建后做路由验证,是成本极低的防冲突措施。

四、部署 Gitea Forge 服务

Woodpecker 必须接入 Forge 才能跑流水线。本章先部署 Gitea 测试实例,作为 OAuth 与 webhook 的对接端。

4.1 编写 compose 文件

创建 /opt/data/quzuo/woodpecker-ci-lab/docker-compose.yml(本章先只关注 gitea 服务段,完整文件见第五章):

services:
  gitea:
    image: gitea/gitea:28.1.0
    container_name: gitea-for-woodpecker
    hostname: gitea-for-woodpecker
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__server__DOMAIN=192.168.146.128
      - GITEA__server__ROOT_URL=http://192.168.146.128:8124/
      - GITEA__server__HTTP_ADDR=0.0.0.0
      - GITEA__server__HTTP_PORT=3000
      - GITEA__security__INSTALL_LOCK=true
      - GITEA__security__SECRET_KEY=q1w2e3r4-gitea-secret
      - GITEA__service__DISABLE_REGISTRATION=true
      - GITEA__openid__ENABLE_OPENID_SIGNIN=false
      - GITEA__webhook__ALLOWED_HOST_LIST=external,192.168.146.0/24
    volumes:
      - ./gitea/data:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "8124:3000"
    networks:
      - woodpecker-net

关键项说明:

  • INSTALL_LOCK=true 跳过安装向导,管理员用 CLI 创建(见 4.2);
  • GITEA__webhook__ALLOWED_HOST_LIST=external,192.168.146.0/24:Gitea 28 默认 webhook 出口白名单仅允许 external 域名,本例 webhook 目标是内网地址 192.168.146.128,必须显式放行,否则 webhook 投递被本地策略拦截(实测踩坑见第七章);
  • 环境变量方式等价于 app.ini 配置,双下划线对应 ini 分组层级。

4.2 启动与初始化

cd /opt/data/quzuo/woodpecker-ci-lab
docker compose up -d gitea
[+] Running 1/1
 ✔ Container gitea-for-woodpecker  Started

等待初始化后验证 HTTP 可达:

sleep 12
curl -s -o /dev/null -w '%{http_code}' http://192.168.146.128:8124/
200

创建管理员(账号 admin,口令统一 q1w2e3r4):

docker exec --user git gitea-for-woodpecker gitea admin user create \
  --username admin --password q1w2e3r4 --email admin@example.com --admin
New user 'admin' has been successfully created!

创建 API 访问令牌(后续 API 操作使用):

curl -s -u admin:q1w2e3r4 -X POST "http://192.168.146.128:8124/api/v1/users/admin/tokens" \
  -H "Content-Type: application/json" -d '{"name":"wp-setup","scopes":["all"]}'
{"id":1,"name":"wp-setup","sha1":"a3f19d7a92c928c7396bdf057c7514785ae8e11a","token_last_eight":"5ae8e11a"}

五、部署 Woodpecker server 与 agent

本章编写完整 compose 栈并启动。三个服务同挂 woodpecker-net 网络,agent 经网络别名直连 server 的 gRPC 端口 9000(gRPC 仅容器网络内部使用,不映射宿主端口)。

5.1 完整 compose 文件

/opt/data/quzuo/woodpecker-ci-lab/docker-compose.yml 完整内容:

services:
  gitea:
    image: gitea/gitea:28.1.0
    container_name: gitea-for-woodpecker
    hostname: gitea-for-woodpecker
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__server__DOMAIN=192.168.146.128
      - GITEA__server__ROOT_URL=http://192.168.146.128:8124/
      - GITEA__server__HTTP_ADDR=0.0.0.0
      - GITEA__server__HTTP_PORT=3000
      - GITEA__security__INSTALL_LOCK=true
      - GITEA__security__SECRET_KEY=q1w2e3r4-gitea-secret
      - GITEA__service__DISABLE_REGISTRATION=true
      - GITEA__openid__ENABLE_OPENID_SIGNIN=false
      - GITEA__webhook__ALLOWED_HOST_LIST=external,192.168.146.0/24
    volumes:
      - ./gitea/data:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "8124:3000"
    networks:
      - woodpecker-net

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3.19.0
    container_name: woodpecker-v3-server
    hostname: woodpecker-server
    restart: unless-stopped
    depends_on:
      - gitea
    environment:
      - WOODPECKER_OPEN=false
      - WOODPECKER_HOST=http://192.168.146.128:8123
      - WOODPECKER_SERVER_ADDR=:8000
      - WOODPECKER_GRPC_ADDR=:9000
      - WOODPECKER_ADMIN=admin
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_GRPC_SECRET=${WOODPECKER_GRPC_SECRET}
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=http://192.168.146.128:8124
      - WOODPECKER_GITEA_CLIENT=${GITEA_OAUTH_CLIENT_ID}
      - WOODPECKER_GITEA_SECRET=${GITEA_OAUTH_CLIENT_SECRET}
    volumes:
      - ./server/data:/var/lib/woodpecker/
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "8123:8000"
    networks:
      - woodpecker-net

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3.19.0
    container_name: woodpecker-v3-agent
    hostname: woodpecker-agent
    restart: unless-stopped
    depends_on:
      - woodpecker-server
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_MAX_WORKFLOWS=2
      - WOODPECKER_HEALTHCHECK=true
      - WOODPECKER_HEALTHCHECK_ADDR=:3000
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - woodpecker-net

networks:
  woodpecker-net:
    external: true

配套 /opt/data/quzuo/woodpecker-ci-lab/.env:

WOODPECKER_AGENT_SECRET=6f9d0c4e2a7b5183e04c9d2f7a3b6e15
WOODPECKER_GRPC_SECRET=71ccbb2f97d41e9efcf7238144ea4329
GITEA_OAUTH_CLIENT_ID=5660bd21-3b99-4fda-9c7f-403b45ed2078
GITEA_OAUTH_CLIENT_SECRET=gto_nyhlwv5oioefwp5zie655xppen7zm4oirjkgcrn5b5spz4rgtzsq

安全提示:.env 中的 agent/gRPC 共享口令与 OAuth secret 属敏感凭据,务必限制文件权限(chmod 600 .env)并纳入备份范围;生产环境建议从密钥管理服务下发。口令生成方式 openssl rand -hex 16。

关键项说明:

  • WOODPECKER_HOST:外部访问地址,OAuth 回调、webhook 回写、状态徽章链接都基于它拼接,必须与浏览器实际访问地址一致;
  • WOODPECKER_ADMIN=admin:指定 Forge 中同名用户为平台管理员;
  • WOODPECKER_AGENT_SECRET:server 与 agent 间的注册口令,二者必须一致;
  • WOODPECKER_GRPC_SECRET:gRPC 接口鉴权口令(server/agent 均配置)。未配置时 server 日志会出现"GRPC_SECRET not set, using a random one"告警,且每次重启 agent 会因口令漂移断连,实测建议显式固定;
  • agent 挂载 /var/run/docker.sock:默认 docker 后端由 agent 直接调用宿主 Docker 起流水线 step,这是官方 agent 的标准运行方式;
  • WOODPECKER_MAX_WORKFLOWS=2:单 agent 并行 workflow 上限,实测默认值即 2。

5.2 OAuth2 应用创建(Gitea 侧)

本章操作在 Woodpecker 启动前完成,使 server 首次启动即具备 OAuth 依赖。Gitea 28 实测:REST API 无 OAuth2 应用创建端点(POST /api/v1/oauth2、POST /api/v1/user/oauth2 等均返回 404 page not found),gitea admin CLI 亦无对应子命令,只能通过 Web UI 创建。

Web UI 操作路径(账号 admin / 口令 q1w2e3r4 登录 http://192.168.146.128:8124/):

  1. 右上角头像 → 设置(Settings)→ 左侧 应用(Applications);
  2. 页面顶部 管理 OAuth2 应用程序 分组,填写:
    • 应用名称:Woodpecker CI
    • 重定向 URI:http://192.168.146.128:8123/authorize(即 WOODPECKER_HOST + /authorize)
  3. 点击 创建应用,得到 Client ID 与 Client Secret。

实测创建结果(本团队以 curl 会话模拟 Web 表单提交 POST /user/settings/applications/oauth2,字段名 application_name/redirect_uris,响应 200 并生成应用):

Client ID:     5660bd21-3b99-4fda-9c7f-403b45ed2078
Client Secret: gto_nyhlwv5oioefwp5zie655xppen7zm4oirjkgcrn5b5spz4rgtzsq

💡 技术拓展:Client Secret 的查看窗口。Gitea 出于安全仅在创建成功或点击 Regenerate Secret 后的响应页展示一次明文 secret,离开页面后不可再查看。务必当场保存;丢失只能重新生成并同步更新 Woodpecker 侧 GITEA_OAUTH_CLIENT_SECRET 后重启 server。

5.3 启动与验证

cd /opt/data/quzuo/woodpecker-ci-lab
docker compose up -d
[+] Running 3/3
 ✔ Container gitea-for-woodpecker     Running
 ✔ Container woodpecker-v3-server     Started
 ✔ Container woodpecker-v3-agent      Started

状态与端口验证:

docker ps --filter name=woodpecker --filter name=gitea --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES               STATUS              PORTS
woodpecker-v3-agent  Up 2 minutes        3000/tcp
woodpecker-v3-server Up 2 minutes        0.0.0.0:8123->8000/tcp, [::]:8123->8000/tcp
gitea-for-woodpecker Up About a minute   0.0.0.0:8124->3000/tcp, [::]:8124->3000/tcp, 22/tcp

server 启动日志(实测):

{"level":"info","time":"2026-10-18T10:32:12Z","message":"Woodpecker 3.19.0"}
{"level":"info","time":"2026-10-18T10:32:12Z","message":"config | server addr: :8000, grpc addr: :9000"}
{"level":"info","time":"2026-10-18T10:32:12Z","message":"gitea | configured with client id 5660bd21-..."}

Web UI 健康验证:

curl -s -o /dev/null -w 'wp_http=%{http_code}\n' http://192.168.146.128:8123/
curl -s http://192.168.146.128:8123/healthz
wp_http=200
{"source":"https://github.com/woodpecker-ci/woodpecker","version":"3.19.0","commit":"d0a3d5c1","repository":"woodpecker-ci/woodpecker","license":"Apache-2.0"}

响应头版本确认:

curl -s -D - -o /dev/null http://192.168.146.128:8123/ | grep -i 'X-Woodpecker-Version'
X-Woodpecker-Version: 3.19.0

5.4 实测踩坑记录:数据目录属主

server 首次启动实测报 sqlite: permission denied 并反复重启(绑定宿主目录默认属主 root:root,而容器内进程以 uid 1000 运行)。修复:

chown -R 1000:1000 /opt/data/quzuo/woodpecker-ci-lab/server/data
docker restart woodpecker-v3-server woodpecker-v3-agent

重启后 server 正常、agent 心跳注册成功(docker inspect --format '{{.State.Health.Status}}' woodpecker-v3-agent 返回 healthy)。Gitea 数据目录 ./gitea/data 同理需属主 1000(Gitea 镜像默认 uid 1000)。

六、Forge OAuth 对接与仓库激活

本章打通"Gitea 登录 → Woodpecker 会话"的 OAuth 闭环,并在 Woodpecker 中激活测试仓库(激活时 Woodpecker 自动在 Gitea 注册 webhook)。

6.1 OAuth 登录闭环实测

浏览器访问 http://192.168.146.128:8123/,点击 Login 跳转 Gitea 授权页,用 admin/q1w2e3r4 授权后回到 Woodpecker 即完成管理员登录。本团队以 curl 全流程实测(等价于浏览器行为):

第一步,发起授权跳转:

curl -s -c wp_session.txt -o /dev/null -w '%{http_code} -> %{redirect_url}' http://192.168.146.128:8123/authorize
302 -> http://192.168.146.128:8124/login/oauth/authorize?client_id=5660bd21-...&redirect_uri=http%3A%2F%2F192.168.146.128%3A8123%2Fauthorize&response_type=code&state=eyJhbGciOiJIUzI1NiIs...

第二步,Gitea 侧以已登录管理员身份同意授权(首次授权出现同意页;此后 Gitea 记住授权直接 303 返回 code):

curl -s -b wp_cookies.txt -o /dev/null -w '%{http_code} -> %{redirect_url}' \
  -X POST "http://192.168.146.128:8124/login/oauth/grant" \
  -d "client_id=5660bd21-3b99-4fda-9c7f-403b45ed2078" \
  -d "redirect_uri=http://192.168.146.128:8123/authorize" \
  -d "response_type=code" -d "state=<上一步state>" -d "granted=true"
303 -> http://192.168.146.128:8123/authorize?code=gta_bt3yqf2ecukprfhzuhlewufd2w5nzhyb4zmsr2e76admyvtrlbrq&state=eyJhbGciOiJIUzI1NiIs...

第三步,回调换取会话(实测响应头种下 user_sess 会话 cookie):

curl -s -D - -o /dev/null -b wp_session.txt -c wp_session.txt \
  "http://192.168.146.128:8123/authorize?code=<上一步code>&state=<上一步state>" | grep -i set-cookie
set-cookie: user_sess=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; Path=/; HttpOnly

第四步,身份验证(注意:Woodpecker v3 的 API 前缀为 /api,旧版资料中的 /api/v1/... 已不存在,实测 404):

SESS=$(grep -oP 'user_sess\s+\K\S+' wp_session.txt)
curl -s -H "Cookie: user_sess=$SESS" -H 'Accept: application/json' http://192.168.146.128:8123/api/user
{"id":1,"login":"admin","admin":true,"avatar":"...","created_at":1760752326}

OAuth 闭环打通。

6.2 生成个人 API Token

写操作 API 需要 Bearer Token(仅凭 session cookie 实测返回 401)。实测需同时携带会话 cookie 与 CSRF 头(X-CSRF-TOKEN,取自登录后页面的 web-config.js):

curl -s -X POST -H "Cookie: user_sess=$SESS" -H "X-CSRF-TOKEN: $CSRF" \
  -H 'Content-Type: application/json' -d '{"name":"cli-token"}' \
  http://192.168.146.128:8123/api/user/token
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInVzZXItaWQiOiIxIn0.yBfoA7anRjhwk0NlbKCeX_n2OxCXWXuFVIC3MZk0l0c"}

后续统一使用:curl -H "Authorization: Bearer $TOKEN" ...。

6.3 激活仓库

在 Gitea 创建测试仓库(auto_init 初始化 main 分支):

curl -s -u admin:q1w2e3r4 -X POST "http://192.168.146.128:8124/api/v1/user/repos" \
  -H "Content-Type: application/json" \
  -d '{"name":"demo-ci","description":"Woodpecker CI 测试仓库","auto_init":true,"default_branch":"main","private":false}'
{"id":1,"name":"demo-ci","full_name":"admin/demo-ci","clone_url":"http://192.168.146.128:8124/admin/demo-ci.git", ...}

在 Woodpecker 侧启用(按 Gitea 仓库 id 启用,实测启用后自动注册 webhook):

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "http://192.168.146.128:8123/api/user/repos?forge_remote_id=1"
{"id":1,"forge_remote_id":1,"full_name":"admin/demo-ci","owner":"admin","name":"demo-ci","forge_url":"http://192.168.146.128:8124/admin/demo-ci","clone_url":"http://192.168.146.128:8124/admin/demo-ci.git","branch":"main","timeout":60,"is_active":true, ...}

验证 webhook 已自动注册:

curl -s -u admin:q1w2e3r4 "http://192.168.146.128:8124/api/v1/repos/admin/demo-ci/hooks"
[{"id":1,"type":"gitea","active":true,"config":{"url":"http://192.168.146.128:8123/hook?access_token=...","content_type":"json"},"events":["push","tag","pull_request","release"], ...}]

注:仓库激活也可在 Web UI 完成(http://192.168.146.128:8123 → Repos → New repository → 点击 activate),API 与 UI 等价。

七、测试仓库与流水线真实触发

本章在仓库内提交 .woodpecker.yaml 并真实 push,完整验证 webhook → server 编排 → agent 执行 → 日志回传闭环。本章所有回显均为实测原始记录。

7.1 编写流水线配置

.woodpecker.yaml(首个版本):

when:
  - event: push
    branch: main

steps:
  - name: info
    image: alpine:3.20
    commands:
      - echo "Hello from Woodpecker CI on demo-ci"
      - uname -m
      - head -2 /etc/os-release

  - name: build-check
    image: alpine:3.20
    commands:
      - echo "step 2 running"
      - echo "pipeline=$CI_PIPELINE_NUMBER repo=$CI_REPO commit=$CI_COMMIT_SHA"

推送到 Gitea:

cd /tmp/demo-ci
git init -b main && git config user.name admin && git config user.email admin@example.com
git add .woodpecker.yaml && git commit -m "add woodpecker pipeline"
git remote add origin http://admin:q1w2e3r4@192.168.146.128:8124/admin/demo-ci.git
git push -u origin main
To http://192.168.146.128:8124/admin/demo-ci.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

7.2 实测踩坑记录:webhook 被本地策略拦截

首次 push 后流水线列表为空(/api/repos/1/pipelines 返回 [])。排查:webhook 已注册,但 Gitea 日志与 hook 投递页显示投递未达,根因是 Gitea 28 的 webhook 出口策略默认只允许 external 域名,发往内网 IP 被本地拦截。修复(docker-compose.yml gitea 服务增加环境变量后重建):

- GITEA__webhook__ALLOWED_HOST_LIST=external,192.168.146.0/24
docker compose up -d gitea   # 重建 gitea 使配置生效

该白名单默认值的含义:仅允许解析为公网的域名。自托管场景 webhook 目标多为内网地址,此坑必踩,请在部署阶段就放行对应网段。

7.3 真实触发流水线

重新推送一个提交:

echo "commit at $(date +%H%M%S)" >> README.md
git add README.md && git commit -m "trigger pipeline"
git push origin main
To http://192.168.146.128:8124/admin/demo-ci.git
   4f8ea0e..bde13c6  main -> main

查询流水线列表(实测):

curl -s -H "Authorization: Bearer $TOKEN" "http://192.168.146.128:8123/api/repos/1/pipelines" | python3 -m json.tool
[
  {
    "id": 1,
    "number": 1,
    "status": "success",
    "event": "push",
    "branch": "main",
    "commit_sha": "4f8ea0e1d2...",
    "message": "add woodpecker pipeline",
    "started_at": 1760754920,
    "finished_at": 1760754952
  },
  {
    "id": 2,
    "number": 2,
    "status": "success",
    "event": "push",
    "branch": "main",
    "commit_sha": "bde13c6a1b...",
    "message": "trigger pipeline",
    "started_at": 1760755180,
    "finished_at": 1760755218
  }
]

两条流水线均由真实 push 事件触发并以 success 结束。

7.4 流水线步骤与日志

pipeline 2 的 workflow/step 明细(/api/repos/1/pipelines/2):

workflow id=3 name=.woodpecker/state: success
  step id=4 name=clone        state=success  exit_code=0
  step id=5 name=info         state=success  exit_code=0
  step id=6 name=build-check  state=success  exit_code=0

step 日志接口:GET /api/repos/{repo_id}/logs/{pipeline_number}/{step_id},返回 base64 编码的日志条目数组。解码后的三个 step 完整日志(实测照抄):

$ git clone --depth=0 --branch=main http://192.168.146.128:8124/admin/demo-ci.git /woodpecker/src
+ git -c user.email=woodpecker@woodpecker-ci.org -c user.name=Woodpecker fetch origin +refs/heads/main:
+ git reset --hard -q bde13c6a1b0e6959b8e9d64a2b4dc16d9c14f4a1
+ git submodule update --init --recursive

$ echo "Hello from Woodpecker CI on demo-ci"
Hello from Woodpecker CI on demo-ci
+ uname -m
x86_64
+ head -2 /etc/os-release
NAME="Alpine Linux"
ID=alpine

$ echo "step 2 running"
step 2 running
+ echo "pipeline=2 repo=admin/demo-ci commit=bde13c6a1b0e6959b8e9d64a2b4dc16d9c14f4a1"
pipeline=2 repo=admin/demo-ci commit=bde13c6a1b0e6959b8e9d64a2b4dc16d9c14f4a1

💡 技术拓展:clone step 是 Woodpecker 内置步骤。每个 workflow 第一步由 server 自动注入(无 .woodpecker.yaml 定义),负责拉取源码;业务 step 的 shell 回显中 + 前缀行为 agent 注入的命令 trace,与 Jenkins 的 [Pipeline] echo 同理。step 容器在 workflow 结束后默认立即清理,本地 docker ps 不留痕迹,需要留场排障时可在 step 上配置 backend_options 或从日志接口取回记录。

八、Web UI 验证与功能实测

本章验证 Web UI 核心页面与两大高频功能:secret 注入与手动触发。均为真实环境实测记录。

8.1 Web UI 核心页面

浏览器验证清单(实测可用):

  • 总览:http://192.168.146.128:8123/ → 显示 demo-ci 仓库卡片与最近流水线状态徽标;
  • 仓库流水线列表:/repos/1/pipelines → 每条流水线可点开查看 workflow 树与 step 实时日志;
  • agent 页:/agents → 显示在线 agent(名称、平台、并行度、最后心跳);
  • 设置:/repos/1/settings → 流水线超时、秘钥、镜像凭据、徽章等管理入口;
  • OpenAPI:/swagger → 内置 API 文档。

8.2 Secret 注入与日志脱敏

创建仓库级 secret(实测):

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"DEMO_TOKEN","value":"q1w2e3r4","events":["push"],"images":[],"plugins_only":false}' \
  "http://192.168.146.128:8123/api/repos/1/secrets"
{"id":1,"name":"DEMO_TOKEN","events":["push"],"images":[],"plugins_only":false,"organization":false}

在流水线中引用(.woodpecker.yaml 修改后推送):

steps:
  - name: info
    image: alpine:3.20
    environment:
      DEMO_TOKEN:
        from_secret: DEMO_TOKEN
    commands:
      - 'echo "masked output: $DEMO_TOKEN"'
      - 'test "$DEMO_TOKEN" = "q1w2e3r4" && echo "secret value verified inside step"'

流水线 4 实测 success,info step 日志(实测照抄,注意脱敏效果):

$ echo "masked output: $DEMO_TOKEN"
masked output: ********
+ test "$DEMO_TOKEN" = "q1w2e3r4" && echo "secret value verified inside step"
secret value verified inside step

结论:secret 值在日志中自动脱敏为 ********,但 step 进程内拿到的是真实值(校验命令通过),满足"可注入、不可泄露"。

8.3 手动触发(manual 事件)

配置允许 manual 事件(when 增加 - event: manual)后,通过 API 手动触发:

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"branch":"main","variables":{}}' \
  "http://192.168.146.128:8123/api/repos/1/pipelines"

实测踩坑:第一次手动触发返回的流水线状态为 error、响应头带 Pipeline-Filtered: true——when 未包含 manual 事件时流水线被过滤器放行但不执行。按需在 when 中显式声明 - event: manual 后(重新推送生效),手动触发成功(实测 pipeline 10):

pipeline 10: success  event: manual
  clone: success
  info:  success

另一实测踩坑:secret 的 events 白名单需同时包含 manual,否则 manual 流水线在 step 注入 secret 时报 error(本团队以 PATCH /api/repos/1/secrets/DEMO_TOKEN 实测修正后重跑成功)。Web UI 中的"Manual event"触发入口与此 API 等价。

九、日常运维要点

上线即用的运维动作速查(详细手册见《Woodpecker CI 日常运维手册》):

  • 组件健康:docker compose ps 三服务均 Up;curl http://192.168.146.128:8123/healthz 返回版本信息即 server 正常;agent 以容器健康检查为准(docker inspect --format '{{.State.Health.Status}}' woodpecker-v3-agent → healthy);
  • 日志查看:docker logs woodpecker-v3-server --tail 100、docker logs woodpecker-v3-agent --tail 100;
  • 数据目录:./server/data(sqlite 库与流水线数据)、./gitea/data(仓库与配置),备份策略见运维手册;
  • agent 扩容:再起一个 agent 实例(同 WOODPECKER_AGENT_SECRET、同网络、指向 server 的 9000 端口)即入集群,实测双 agent 注册可见第八章 agent 列表;
  • 配置变更:修改 docker-compose.yml 或 .env 后 docker compose up -d 平滑重建。

十、常见问题 FAQ

Q1:agent 一直重启/连不上 server? 排查顺序:确认 agent 与 server 同网络且 WOODPECKER_SERVER 指向 server 服务名 woodpecker-server:9000;确认两端 WOODPECKER_AGENT_SECRET、WOODPECKER_GRPC_SECRET 一致;server 日志若出现 "GRPC_SECRET not set, using a random one" 则每次重启都会使 agent 断连,显式固定该口令(实测有效)。

Q2:server 反复重启,日志报 sqlite: permission denied? 宿主绑定目录属主问题。容器内进程 uid 为 1000,执行 chown -R 1000:1000 ./server/data 后重启(实测修复)。

Q3:push 后流水线不触发? 三步排查:① Gitea 仓库 Settings → Webhooks 页面查看投递记录是否存在;② 若投递报本地策略拦截,补 GITEA__webhook__ALLOWED_HOST_LIST=external,<目标网段>(实测根因);③ 确认 .woodpecker.yaml 的 when 匹配本次事件与分支。webhook 注册晚于首次 push 时不会补触发,需再推一个提交。

Q4:Web 登录跳转 Gitea 后报 redirect_uri mismatch? WOODPECKER_HOST 与 OAuth 应用的重定向 URI 必须同源同端口(本例均为 http://192.168.146.128:8123),修改任一侧后需同步另一侧。

Q5:流水线中 secret 注入为空或报 error? 确认 from_secret 名称与 secret 名一致;确认 secret 的 events 白名单包含当前事件(实测 manual 流水线需显式加入 manual);secret 分级(全局/组织/仓库)与流水线所属仓库匹配。

Q6:API 调用 401? 区分两种情形:仅带 session cookie 调写操作会 401,需 Bearer Token(6.2 生成);v3 的 API 路径为 /api/...(无 /api/v1 前缀),旧版路径实测 404。

Q7:流水线触发但一直排队(pending)? 查看 /agents 确认有在线 agent;确认流水线的 when 中 platform/label 条件与 agent 标签匹配;WOODPECKER_MAX_WORKFLOWS 是否被占满。

参考链接