前情提要 Docker 部署 Gitea/Forgejo 个人代码托管服务

选择集成服务

Gitea/Forgejo 除了轻量还拥有强大的 CI/CD 集成支持,我暂时了解的选择项:

  • Gitea Actions: 基于 nektos/act 的分叉,称作 act runner ,是官方的默认推荐。与 GitHub Actions 高度兼容,但不适合我的实际需求;
  • Drone CI: 被商业收购后,开源版几乎停滞且充满混乱,不建议使用;
  • Woodpecker CI: Drone CI 的 OSS Fork,社区活跃,简单易用。

三者都能与 Gitea 轻松集成,最终我选择了 Woodpecker CI。

基本概念

Woodpecker CI 基本服务组成为 serveragent 及可选的 autoscaler,前两者是必需的,工作流程如下:

                        ┌──────────────────────┐
                        │   Code Hosting /     │   <-- webhooks, events
                        │   Forge (e.g. Gitea) │      push / pr / commit
                        └─────────┬────────────┘
                                  │ HTTP / Webhook
                        ┌──────────────────────┐
                        │   Woodpecker Server  │
                        │    API / Web UI      │
                        │    orchestration     │
                        └─────────┬────────────┘
                                  │ gRPC (queue / job dispatch)
         ┌────────────┬──────────────┼──────────────┬────────────┐
         │            │                             │            │
         ▼            ▼                             ▼            ▼
┌────────────────┐ ┌────────────────┐        ┌────────────────┐ (more agents …)
│ Woodpecker     │ │ Woodpecker     │        │ Woodpecker     │
│ Agent (Docker) │ │ Agent (Docker) │  …     │ Agent (K8s /   │
│                │ │                │        │ Docker / local)└────────────────┘ └────────────────┘        └────────────────┘
         │                 │                          │
         │ Executes        │ Executes                 │ Executes
         │ pipeline steps  │ pipeline steps           │ pipeline steps
(build / test / │ (build / test /          │ (build / test / 
         │  deploy etc.)   │  deploy etc.)            │  deploy etc.) 
         ▼                 ▼                          ▼
   build logs / status  build logs / status      build logs / status
         └──────────────┬──────────────┬──────────────┘
                        │  Results / statuses sent back to Server
                Server updates Web UI, sends status back to Forge

(Optional) ─────────────────────────────────────────────────────────────
(monitors queue / load)
      ┌────────────────────────────┐
      │ Woodpecker Autoscaler      │
      └────────────┬───────────────┘
                   │   dynamically spin up / down VM(s) / Agent host(s)
           New Agent(s)       <-- connect via gRPC to Server

注册 OAuth2 应用

在 Gitea 实例注册 OAuth2 应用,获取 Client IDClient Secret 备用

create-oauth2-app.webp

部署

更新 Docker 模板

在 Gitea 的 Compose 模板里,加入服务片段。对于单人实例,建议使用 SQLite 数据库,看起来就像这样:

services:
  gitea:
    image: docker.gitea.com/gitea:1.25.2-rootless
    container_name: gitea
    restart: unless-stopped
    user: "1000"
    volumes:
      - ./data:/var/lib/gitea
      - ./config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:33033:3000"
      - "127.0.0.1:11211:2222"
    networks:
      - gitea

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    container_name: woodpecker-server
    restart: unless-stopped
    depends_on:
      gitea:
        condition: service_healthy
    ports:
      - '127.0.0.1:18000:8000'
    networks:
      - gitea
    volumes:
      - ./woodpecker/server/data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=false # disable registration
      - WOODPECKER_ADMIN=${WOODPECKER_ADMIN}
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_DISABLE_VERSION_CHECK=false
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=${WOODPECKER_GITEA_URL}
      - WOODPECKER_GITEA_SKIP_VERIFY=false
      - WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
      - WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    container_name: woodpecker-agent
    restart: unless-stopped
    depends_on:
      woodpecker-server:
        condition: service_healthy
    networks:
      - gitea
    volumes:
      - ./woodpecker/agent/config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_MAX_WORKFLOWS=2

networks:
  gitea:

准备 Docker 绑定卷

提前建立 Woodpecker 所需数据持久化目录

mkdir -p ./woodpecker/{server/data,agent/config}

设置用户组权限

sudo chown -R 1000:1000 ./woodpecker
sudo chmod -R 755 ./woodpecker

配置环境变量

  • WOODPECKER_ADMIN 对应 Gitea 实例上允许授权的用户名

  • WOODPECKER_GITEA_URL 填写 Gitea 实例 URL,如 https://git.your.domain

  • WOODPECKER_GITEA_CLIENT 是 Gitea OAuth2 应用的 Client ID

  • WOODPECKER_GITEA_SECRET 是 Gitea OAuth2 应用的 Client Secret

  • WOODPECKER_AGENT_SECRET 随机值机密,使用 openssl rand -base64 32 生成

Gitea WebHook 配置

编辑 ./data/app.ini ,设置 Gitea 允许的 WebHook 网络主机

[webhook]
ALLOWED_HOST_LIST=external,loopback

启动服务

模板和环境变量准备好后,启动服务

sudo docker compose up -d

Nginx 反向代理

下面是 Woodpecker 的 Nginx 反向代理配置示例,如果图方便和安全,更推荐使用 Cloudflare Tunnel 访问服务

server {
    listen 80;
    listen [::]:80;
    server_name woodpecker.your.domain;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name woodpecker.your.domain;

    ssl_certificate /path/to/cert/wild.via.moe.pem;
    ssl_certificate_key /path/to/cert/wild.via.moe.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:P-256:P-384;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256';
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    access_log /var/log/nginx/woodpecker.your.domain.access.log;
    error_log /var/log/nginx/woodpecker.your.domain.error.log;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;

    location / {
        proxy_pass http://127.0.0.1:18000;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $realip_remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_redirect off;
        proxy_buffering off;
        chunked_transfer_encoding off;
    }
}

将域名解析到服务器 IP,重载 Nginx 配置

sudo nginx -t
sudo nginx -s reload

授权访问

访问 Woodpecker 域名,进入 Gitea 应用授权页面

oauth.webp

授权成功,会进入 Woodpecker 管理台

woodpecker.webp

测试

最后,简单测试下 CI 是否正常工作。在存储库添加 CI 脚本 .woodpecker/ci-test.yaml

when:
  - event: push
    branch: main

steps:
  - name: node-build
    image: node:20-alpine
    commands:
      - echo "Running Node.js test..."
      - node -e 'console.log("Hello, world! from Node.js")'
      - echo "CI test success! Dejavu Moe says hello to you"

Woodpecker 添加这个存储库后,回到 Gitea 存储库任意提交一次。

ci-test.webp

在 Gitea 也可以看到它正常工作

finish.webp

一切完成!

参考资料: