How to Self-Host Woodpecker CI: Lightweight CI/CD for Self-Hosted Git 2026
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/*.ymlfiles 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
| Feature | Woodpecker | Drone | Gitea Actions |
|---|---|---|---|
| License | Apache 2.0 | Apache 2.0 | Built-in to Gitea |
| GitHub Stars | ~4K | ~31K | — |
| Forked from | Drone | — | — |
| GitHub Actions compat | No | No | Yes |
| Gitea/Forgejo native | Yes (primary) | Yes | Yes |
| Multi-pipeline files | Yes | No | Yes |
| Matrix builds | Yes | Yes | Yes |
| RAM usage | ~50MB | ~100MB | — |
| Cron triggers | Yes | Yes | Yes |
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
- Gitea: User Settings → Applications → OAuth2 Applications → Create
- Name:
Woodpecker CI - Redirect URI:
https://ci.yourdomain.com/authorize - 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.