Skip to main content

How to Self-Host Woodpecker CI: Lightweight CI/CD for Self-Hosted Git 2026

·OSSAlt Team
woodpeckerci-cddevopsself-hostingdockergiteaforgejo2026

TL;DR

Woodpecker CI (Apache 2.0, ~4K GitHub stars, Go) is a community fork of Drone CI, purpose-built for Gitea and Forgejo. It's lighter than Drone, actively maintained, and has a cleaner integration with self-hosted Git forges. Pipelines are defined in .woodpecker.yml, every step runs in Docker, and the server + agent take under 50MB RAM combined. If you're running Gitea or Forgejo and want a simple CI that just works, Woodpecker is the answer.

Key Takeaways

  • Woodpecker CI: Apache 2.0, ~4K stars, Go — Drone fork optimized for Gitea/Forgejo
  • Multi-pipeline: Multiple .woodpecker/*.yml files per repo (split by service/workflow)
  • Docker + local agents: Docker agent for containerized builds, local agent for bare-metal
  • Gitea/Forgejo native: First-class integration, auto-creates webhooks
  • Cron triggers: Schedule pipelines on a cron expression
  • Matrix builds: Run same pipeline across multiple versions/environments in parallel

Woodpecker vs Drone vs Gitea Actions

FeatureWoodpeckerDroneGitea Actions
LicenseApache 2.0Apache 2.0Built-in to Gitea
GitHub Stars~4K~31K
Forked fromDrone
GitHub Actions compatNoNoYes
Gitea/Forgejo nativeYes (primary)YesYes
Multi-pipeline filesYesNoYes
Matrix buildsYesYesYes
RAM usage~50MB~100MB
Cron triggersYesYesYes

Part 1: Docker Setup

# docker-compose.yml
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker_server
    restart: unless-stopped
    ports:
      - "8000:8000"
      - "9000:9000"   # gRPC port for agents
    volumes:
      - woodpecker_data:/var/lib/woodpecker
    environment:
      WOODPECKER_OPEN: "false"               # Disable open registration
      WOODPECKER_HOST: "https://ci.yourdomain.com"
      WOODPECKER_GITEA: "true"
      WOODPECKER_GITEA_URL: "https://git.yourdomain.com"
      WOODPECKER_GITEA_CLIENT: "${GITEA_CLIENT_ID}"
      WOODPECKER_GITEA_SECRET: "${GITEA_CLIENT_SECRET}"
      WOODPECKER_AGENT_SECRET: "${AGENT_SECRET}"
      # Admin users (Gitea usernames, comma-separated):
      WOODPECKER_ADMIN: "alice,bob"
      # Database:
      WOODPECKER_DATABASE_DRIVER: "sqlite3"
      WOODPECKER_DATABASE_DATASOURCE: "/var/lib/woodpecker/woodpecker.db"
      # Logging:
      WOODPECKER_LOG_LEVEL: "info"

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker_agent
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - woodpecker_agent_data:/etc/woodpecker
    environment:
      WOODPECKER_SERVER: "woodpecker-server:9000"
      WOODPECKER_AGENT_SECRET: "${AGENT_SECRET}"
      WOODPECKER_MAX_PROCS: "2"     # Concurrent jobs
      WOODPECKER_AGENT_NAME: "agent-1"
      WOODPECKER_BACKEND: "docker"
    depends_on:
      - woodpecker-server

volumes:
  woodpecker_data:
  woodpecker_agent_data:
# .env
AGENT_SECRET=$(openssl rand -hex 32)
# GITEA_CLIENT_ID and GITEA_CLIENT_SECRET from Gitea OAuth2 app

docker compose up -d

Part 2: HTTPS with Caddy

ci.yourdomain.com {
    reverse_proxy localhost:8000
}

Part 3: Gitea OAuth2 Setup

  1. Gitea: User Settings → Applications → OAuth2 Applications → Create
  2. Name: Woodpecker CI
  3. Redirect URI: https://ci.yourdomain.com/authorize
  4. Copy Client ID and Secret to .env

Part 4: Pipeline YAML

Create .woodpecker.yml (or .woodpecker/*.yml for multi-pipeline) in your repo:

Basic build

# .woodpecker.yml
steps:
  - name: test
    image: node:20-alpine
    commands:
      - npm ci
      - npm test

  - name: build
    image: node:20-alpine
    commands:
      - npm run build
    when:
      branch: main

Multi-file pipelines

.woodpecker/
  test.yml        ← Runs on every push
  deploy.yml      ← Runs only on main
  cron.yml        ← Scheduled jobs
# .woodpecker/test.yml
when:
  event: [push, pull_request]

steps:
  - name: lint
    image: node:20-alpine
    commands:
      - npm ci
      - npm run lint

  - name: test
    image: node:20-alpine
    commands:
      - npm test
# .woodpecker/deploy.yml
when:
  branch: main
  event: push

steps:
  - name: build-image
    image: woodpeckerci/plugin-docker-buildx:latest
    settings:
      registry: git.yourdomain.com
      repo: git.yourdomain.com/myorg/myapp
      tags: latest,${CI_COMMIT_SHA:0:8}
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password

  - name: deploy
    image: alpine
    environment:
      SSH_KEY:
        from_secret: deploy_key
    commands:
      - apk add openssh
      - echo "$SSH_KEY" > /tmp/key && chmod 600 /tmp/key
      - ssh -i /tmp/key deploy@prod.yourdomain.com "docker compose pull && docker compose up -d"

Matrix builds

Run pipeline across multiple Node versions:

matrix:
  NODE_VERSION:
    - 18
    - 20
    - 22

steps:
  - name: test
    image: "node:${NODE_VERSION}-alpine"
    commands:
      - npm ci
      - npm test

Cron schedule

# .woodpecker/cron.yml
when:
  event: cron
  cron: nightly-backup

steps:
  - name: backup
    image: alpine
    commands:
      - ./scripts/backup.sh

Enable cron in Woodpecker UI: Repo Settings → Cron → Add cron → name: nightly-backup, schedule: 0 2 * * *


Part 5: Secrets

# Via Woodpecker CLI:
# Install: brew install woodpecker-ci/tap/woodpecker-cli (or download binary)

export WOODPECKER_SERVER=https://ci.yourdomain.com
export WOODPECKER_TOKEN=your-api-token   # From UI → Account → Token

# Add repo secret:
woodpecker secret add \
  --repository myorg/myrepo \
  --name docker_password \
  --value "your-password" \
  --event push --event pull_request

# List secrets:
woodpecker secret ls --repository myorg/myrepo

# Add org secret (shared across all org repos):
woodpecker secret add \
  --organization myorg \
  --name docker_password \
  --value "your-password"

Use in pipeline:

settings:
  password:
    from_secret: docker_password

# Or as environment variable:
environment:
  API_KEY:
    from_secret: api_key

Part 6: Scale with Multiple Agents

Add more agents on the same or different machines:

# On a second machine (separate docker-compose.yml):
services:
  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker_agent_2
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      WOODPECKER_SERVER: "ci.yourdomain.com:9000"   # gRPC endpoint
      WOODPECKER_AGENT_SECRET: "your-agent-secret"
      WOODPECKER_MAX_PROCS: "4"
      WOODPECKER_AGENT_NAME: "agent-2-powerful"
      WOODPECKER_BACKEND: "docker"

Target specific agents with labels:

# In agent env:
WOODPECKER_AGENT_LABELS: "arch=arm64,gpu=true"

# In pipeline:
labels:
  arch: arm64

Part 7: PostgreSQL for Production

For teams running many pipelines, switch from SQLite:

services:
  woodpecker-server:
    environment:
      WOODPECKER_DATABASE_DRIVER: "postgres"
      WOODPECKER_DATABASE_DATASOURCE: "postgres://woodpecker:${POSTGRES_PASSWORD}@postgres:5432/woodpecker?sslmode=disable"
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: woodpecker
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DB: woodpecker
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U woodpecker"]
      interval: 10s
      start_period: 20s

Maintenance

# Update Woodpecker:
docker compose pull
docker compose up -d

# Backup:
tar -czf woodpecker-backup-$(date +%Y%m%d).tar.gz \
  $(docker volume inspect woodpecker_woodpecker_data --format '{{.Mountpoint}}')

# Logs:
docker compose logs -f woodpecker-server
docker compose logs -f woodpecker-agent

# Check agents:
# UI → Administration → Agents

See all open source CI/CD and DevOps tools at OSSAlt.com/categories/devops.

Comments