The architecture
- Server — receives webhooks from your Git forge, schedules pipelines, serves the web UI. SQLite by default; Postgres / MySQL for production.
- Agent — runs the actual pipeline steps. One or more; each executes containers via Docker / Podman / Kubernetes / SSH / a local binary backend.
- CLI —
woodpecker-clifor triggering pipelines, fetching logs, managing secrets.
Install via docker compose
# docker-compose.yml
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000"
- "127.0.0.1:9000:9000" # grpc for agents
volumes:
- ./server-data:/var/lib/woodpecker
environment:
WOODPECKER_OPEN: "false" # require admin approval for new users
WOODPECKER_ADMIN: "amir"
WOODPECKER_HOST: https://ci.example.com
WOODPECKER_AGENT_SECRET: ${AGENT_SECRET}
# Gitea / Forgejo connection
WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: https://gitea.example.com
WOODPECKER_GITEA_CLIENT: ${GITEA_OAUTH_CLIENT}
WOODPECKER_GITEA_SECRET: ${GITEA_OAUTH_SECRET}
WOODPECKER_DATABASE_DRIVER: postgres
WOODPECKER_DATABASE_DATASOURCE: postgres://wood:${DB_PW}@db/woodpecker?sslmode=disable
depends_on: [ db ]
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WOODPECKER_SERVER: woodpecker-server:9000
WOODPECKER_AGENT_SECRET: ${AGENT_SECRET}
WOODPECKER_MAX_WORKFLOWS: 4
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: wood
POSTGRES_PASSWORD: ${DB_PW}
POSTGRES_DB: woodpecker
volumes:
- ./db-data:/var/lib/postgresql/data
Setup OAuth in Forgejo / Gitea
In Forgejo / Gitea Admin Panel → OAuth2 Applications, create a new app:
- Name:
Woodpecker CI - Redirect URI:
https://ci.example.com/authorize - Save; copy Client ID + Client Secret to the env vars above
docker compose up -d
docker compose logs -f woodpecker-server
Browse to https://ci.example.com; log in via the Gitea OAuth flow; you're in.
The first pipeline
In any Git repo, add .woodpecker.yml (or .woodpecker/*.yml for multi-file pipelines):
# .woodpecker.yml
when:
- event: push
branch: [main, develop]
- event: pull_request
steps:
- name: install
image: node:22-alpine
commands:
- npm ci
- name: lint
image: node:22-alpine
commands:
- npm run lint
- name: test
image: node:22-alpine
commands:
- npm test
environment:
CI: "true"
- name: build
image: node:22-alpine
commands:
- npm run build
when:
- event: push
branch: main
- name: docker
image: woodpeckerci/plugin-docker-buildx
settings:
registry: ghcr.io
repo: ghcr.io/myorg/myapp
tags:
- latest
- ${CI_COMMIT_SHA}
username:
from_secret: ghcr_user
password:
from_secret: ghcr_token
when:
- event: push
branch: main
Commit + push; Woodpecker picks up the webhook, schedules the pipeline. UI shows step-by-step logs in real time.
Services: long-running sidecars
For tests needing a database, Woodpecker's services spawn alongside the pipeline:
services:
- name: postgres
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
- name: redis
image: redis:7-alpine
steps:
- name: test
image: golang:1.23
commands:
- go test ./...
environment:
DATABASE_URL: postgres://postgres:test@postgres:5432/testdb
REDIS_URL: redis://redis:6379
Services share a network with the pipeline steps; postgres / redis are reachable by their service name.
Plugins
Plugins are container images that take a settings block and perform a specific action: build + push Docker images, deploy to Kubernetes, send Slack notifications, upload to S3. Use them by image: ... + settings: { ... }. The Woodpecker community maintains a registry; you can write your own (any container that reads $PLUGIN_* env vars).
Secrets
# Via CLI
woodpecker-cli secret add --repository myorg/myapp \
--name ghcr_token --value ghp_...
# Or in the web UI: Repository → Settings → Secrets
# Referenced in pipeline as
password:
from_secret: ghcr_token
Per-repo, per-organization, or global secrets. Optional event filtering (only available on push, not on pull_request — protects fork PRs from accessing secrets).
Matrix builds
# Run the same steps across multiple Node versions
matrix:
NODE_VERSION:
- 20
- 22
- 23
steps:
- name: test
image: node:${NODE_VERSION}-alpine
commands:
- npm ci && npm test
Multi-pipeline workflows
For complex repos, split into multiple files under .woodpecker/:
.woodpecker/
01-lint.yml
02-test.yml
03-build.yml
04-deploy.yml
Each file is an independent pipeline; declare dependencies with depends_on:. Allows parallel + sequential composition.
Multiple agents + Kubernetes backend
For more parallelism, run more agents (each handles N pipelines simultaneously based on WOODPECKER_MAX_WORKFLOWS). For very large fleets, use the Kubernetes backend — each step runs as a pod, scheduled across cluster nodes.
# Agent env for K8s backend
WOODPECKER_BACKEND: kubernetes
WOODPECKER_BACKEND_K8S_NAMESPACE: woodpecker
WOODPECKER_BACKEND_K8S_STORAGE_CLASS: standard
Woodpecker vs alternatives
- Drone CI — Woodpecker is the community fork after Drone's restrictive license changes. Same YAML format; same plugin ecosystem. Woodpecker has been ahead of Drone on features for 2+ years.
- GitHub Actions (with self-hosted runners) — bigger ecosystem; tighter GitHub integration. Heavier; the act-runner project lets self-hosted GitHub Actions run on Forgejo / Gitea too.
- GitLab CI — bundled with GitLab; great if you're on GitLab; not portable.
- Jenkins — the elder; vastly more feature surface; correspondingly heavier and crustier. Don't pick for new self-hosted setups in 2026.
- Forgejo Actions — Forgejo's bundled CI, GitHub Actions-compatible YAML. The right pick if you already run Forgejo and want zero extra services.
For "small team, self-hosted Forgejo / Gitea, wants real CI without operational drama," Woodpecker is the right size in 2026.