TL;DR — Quick Summary
Deploy Woodpecker CI for lightweight self-hosted CI/CD. Configure pipelines, secrets, agents, and forge integration with Gitea, GitHub, and GitLab.
Woodpecker CI is a lightweight, self-hosted CI/CD system forked from Drone CI that runs container-native pipelines with minimal resource usage. This guide covers complete installation, Gitea forge integration, pipeline syntax, secrets management, agents, and a practical Go application deployment pipeline.
Prerequisites
- Linux server with Docker and Docker Compose.
- At least 512 MB RAM for server + agent (1 GB recommended).
- A running Gitea, Forgejo, GitHub, or GitLab instance for OAuth.
- Ports 8000 (HTTP) and 9000 (gRPC agent communication) open.
Step 1: Woodpecker Architecture
Woodpecker follows a server + agent model:
- Server — Handles the web UI, API, webhook reception, and pipeline scheduling. Stores state in SQLite (default) or PostgreSQL.
- Agent — Polls the server for work and executes pipeline steps inside containers. Multiple agents can connect to one server for parallel builds.
- Forge — The Git hosting provider (Gitea, Forgejo, GitHub, GitLab, Bitbucket) that sends webhooks and provides OAuth authentication.
- Pipeline — Defined in
.woodpecker.ymlat the repository root. Each pipeline is a set of steps executed sequentially (or in parallel) inside Docker containers.
When you push a commit, the forge sends a webhook to the Woodpecker server. The server queues a build and an agent picks it up, pulling container images and running steps in order.
Step 2: Install with Docker Compose
Create the Gitea OAuth App
In Gitea: Settings → Applications → OAuth2 Applications → Create OAuth2 Application
- Application Name: Woodpecker CI
- Redirect URI:
http://your-woodpecker-server:8000/authorize
Copy the Client ID and Client Secret.
Docker Compose Configuration
# docker-compose.yml
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: always
ports:
- "8000:8000"
- "9000:9000"
volumes:
- woodpecker-server-data:/var/lib/woodpecker/
environment:
- WOODPECKER_OPEN=false
- WOODPECKER_HOST=http://your-woodpecker-server:8000
- WOODPECKER_GITEA=true
- WOODPECKER_GITEA_URL=http://your-gitea-server:3000
- WOODPECKER_GITEA_CLIENT=your-gitea-oauth-client-id
- WOODPECKER_GITEA_SECRET=your-gitea-oauth-client-secret
- WOODPECKER_AGENT_SECRET=a-strong-random-secret-for-agents
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: always
depends_on:
- woodpecker-server
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET=a-strong-random-secret-for-agents
- WOODPECKER_MAX_PROCS=2
volumes:
woodpecker-server-data:
docker compose up -d
Access the Woodpecker UI at http://your-woodpecker-server:8000 and log in with Gitea OAuth.
Kubernetes with Helm
helm repo add woodpecker https://woodpecker-ci.org/helm/
helm repo update
helm install woodpecker woodpecker/woodpecker \
--set server.env.WOODPECKER_GITEA=true \
--set server.env.WOODPECKER_GITEA_URL=https://gitea.example.com \
--set server.env.WOODPECKER_GITEA_CLIENT=CLIENT_ID \
--set server.env.WOODPECKER_GITEA_SECRET=CLIENT_SECRET \
--set agent.env.WOODPECKER_AGENT_SECRET=AGENT_SECRET
Step 3: Pipeline Syntax (.woodpecker.yml)
Woodpecker pipelines live in .woodpecker.yml (or a .woodpecker/ directory for multi-pipeline setups).
Basic Pipeline
# .woodpecker.yml
steps:
- name: test
image: golang:1.22
commands:
- go test ./...
- name: build
image: golang:1.22
commands:
- go build -o app .
- name: notify
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: builds
when:
- status: [success, failure]
Conditional Execution with when
steps:
- name: deploy
image: alpine
commands:
- ./deploy.sh
when:
- branch: main
event: push
- event: tag
Services (Database, Redis)
services:
- name: postgres
image: postgres:16
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
steps:
- name: test
image: golang:1.22
environment:
DATABASE_URL: "postgres://test:test@postgres/testdb?sslmode=disable"
commands:
- go test -tags integration ./...
Matrix Builds
matrix:
GO_VERSION:
- "1.21"
- "1.22"
OS:
- linux/amd64
- linux/arm64
steps:
- name: test
image: "golang:${GO_VERSION}"
commands:
- go test ./...
Multi-Pipeline with Path Conditions
Place multiple files in .woodpecker/:
# .woodpecker/backend.yml
when:
- path: "backend/**"
steps:
- name: test-backend
image: golang:1.22
commands:
- cd backend && go test ./...
# .woodpecker/frontend.yml
when:
- path: "frontend/**"
steps:
- name: test-frontend
image: node:22
commands:
- cd frontend && npm ci && npm test
Step 4: Secrets Management
Woodpecker has three secret scopes:
| Scope | Set by | Visible to |
|---|---|---|
| Repository | Repo owner | That repository only |
| Organization | Org admin | All repos in the org |
| Global | Server admin | All repositories |
Add a Repository Secret
In the Woodpecker UI: Repository → Settings → Secrets → Add secret
Reference it in your pipeline:
steps:
- name: deploy
image: alpine
environment:
DEPLOY_KEY:
from_secret: deploy_key
commands:
- echo "$DEPLOY_KEY" | base64 -d > /tmp/key
- ./deploy.sh
External Secret Backends
Woodpecker supports pulling secrets from external systems:
# HashiCorp Vault backend
WOODPECKER_SECRET_SERVICE=vault
WOODPECKER_VAULT_ADDR=http://vault:8200
WOODPECKER_VAULT_TOKEN=hvs.your-vault-token
Step 5: Plugins and Custom Plugins
Woodpecker plugins are standard Docker images. Common plugins:
| Plugin | Image | Purpose |
|---|---|---|
| Docker | woodpeckerci/plugin-docker-buildx | Build and push Docker images |
| Git | woodpeckerci/plugin-git | Clone with submodules |
| S3 | woodpeckerci/plugin-s3 | Upload artifacts to S3 |
| Slack | plugins/slack | Send build notifications |
| Telegram | appleboy/drone-telegram | Telegram build alerts |
| Webhook | woodpeckerci/plugin-webhook | POST to arbitrary URLs |
Writing a Custom Plugin
Any Docker image that reads PLUGIN_* environment variables and exits 0 on success is a Woodpecker plugin:
FROM alpine:3.19
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# Pipeline using custom plugin
steps:
- name: custom-deploy
image: your-registry/custom-deploy-plugin:latest
settings:
target_host:
from_secret: deploy_host
artifact_path: ./dist
Step 6: Agents, CLI, and Multi-Platform Builds
Agent Labels for Routing
Assign labels to agents and route pipelines accordingly:
# Agent with labels
WOODPECKER_FILTER_LABELS=platform=linux/arm64,type=build
# Pipeline targeting ARM64 agent
labels:
platform: linux/arm64
steps:
- name: build-arm
image: golang:1.22
commands:
- GOARCH=arm64 go build -o app-arm64 .
Kubernetes Backend
The Kubernetes backend runs each pipeline step as an ephemeral pod:
WOODPECKER_BACKEND=kubernetes
WOODPECKER_BACKEND_K8S_NAMESPACE=woodpecker
WOODPECKER_BACKEND_K8S_STORAGE_CLASS=standard
woodpecker-cli
# Install
curl -sL https://github.com/woodpecker-ci/woodpecker/releases/latest/download/woodpecker-cli-linux-amd64 \
-o /usr/local/bin/woodpecker
chmod +x /usr/local/bin/woodpecker
# Configure
export WOODPECKER_SERVER=http://your-server:8000
export WOODPECKER_TOKEN=your-api-token
# Manage pipelines
woodpecker pipeline ls owner/repo
woodpecker pipeline start owner/repo --branch main
woodpecker pipeline stop owner/repo 42
# Manage secrets
woodpecker secret add --repository owner/repo --name deploy_key --value "$(cat key.pem)"
woodpecker secret ls --repository owner/repo
# Sync repository
woodpecker repo sync
Cron Jobs (Scheduled Pipelines)
In the Woodpecker UI: Repository → Settings → Crons → Add cron
Or via CLI:
woodpecker cron add --repository owner/repo --name nightly-build \
--expr "0 2 * * *" --branch main
Multi-Platform Builds with QEMU
steps:
- name: build-multiplatform
image: woodpeckerci/plugin-docker-buildx
privileged: true
settings:
repo: registry.example.com/myapp
platforms: "linux/amd64,linux/arm64"
tags: latest
username:
from_secret: registry_user
password:
from_secret: registry_password
Woodpecker vs Alternatives
| Feature | Woodpecker | Drone OSS | Gitea Actions | Jenkins | Concourse |
|---|---|---|---|---|---|
| License | MIT | Apache 2.0 | MIT | MIT | Apache 2.0 |
| RAM (idle) | ~50 MB | ~80 MB | ~100 MB | ~500 MB | ~200 MB |
| Pipeline format | YAML | YAML | YAML (GHA-compat) | Groovy/YAML | YAML |
| Container-native | Yes | Yes | Yes | Optional | Yes |
| Matrix builds | Yes | No (OSS) | Yes | Yes | Yes |
| Multi-pipeline | Yes | No (OSS) | Yes | Yes | No |
| Secrets backends | Vault, AWS | No | No | Many | Vault |
| Forge integrations | 6 forges | 4 forges | Gitea/Forgejo | 10+ | GitHub |
| Kubernetes backend | Yes | Yes | No | Yes | No |
| Active maintenance | Yes | Limited | Yes | Yes | Yes |
Practical Example: Go Application Pipeline
A complete pipeline that tests, builds a Docker image, and deploys a Go application:
# .woodpecker.yml
steps:
- name: test
image: golang:1.22
environment:
CGO_ENABLED: "0"
commands:
- go vet ./...
- go test -race -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
- name: build-binary
image: golang:1.22
environment:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: amd64
commands:
- go build -ldflags="-s -w -X main.version=${CI_COMMIT_SHA}" -o dist/app .
when:
- branch: [main, develop]
event: push
- name: build-docker
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.example.com/myapp
tags:
- latest
- "${CI_COMMIT_SHA:0:8}"
platforms: "linux/amd64,linux/arm64"
username:
from_secret: registry_user
password:
from_secret: registry_password
when:
- branch: main
event: push
- name: deploy
image: alpine
environment:
DEPLOY_KEY:
from_secret: deploy_ssh_key
DEPLOY_HOST:
from_secret: deploy_host
commands:
- apk add --no-cache openssh-client
- echo "$DEPLOY_KEY" | base64 -d > /tmp/key && chmod 600 /tmp/key
- ssh -i /tmp/key -o StrictHostKeyChecking=no deploy@$DEPLOY_HOST
"docker pull registry.example.com/myapp:latest &&
docker stop myapp || true &&
docker run -d --name myapp --rm -p 8080:8080 registry.example.com/myapp:latest"
when:
- branch: main
event: push
- name: notify
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: deployments
template: "Deploy of *myapp* `${CI_COMMIT_SHA:0:8}` to production: {{#success build.status}}succeeded{{else}}FAILED{{/success}}"
when:
- status: [success, failure]
event: push
branch: main
Gotchas and Edge Cases
- Agent secret mismatch — Server and agent must share the same
WOODPECKER_AGENT_SECRET. A mismatch causes the agent to silently fail to connect with no error in the UI. - Docker socket permissions — The agent container needs
/var/run/docker.sockmounted. On some systems you must add the agent to thedockergroup or runchmod 666 /var/run/docker.sock. - Privileged steps — Steps using Docker-in-Docker or QEMU cross-compilation require
privileged: true. Only use this for trusted repositories. - SQLite in production — The default SQLite backend is fine for small teams. Switch to PostgreSQL at
WOODPECKER_DATABASE_DRIVER=postgresfor 5+ concurrent users. - Webhook delivery — Woodpecker requires the server to be reachable from the forge. When running behind NAT, use a tunnel (Cloudflare Tunnel, ngrok) or ensure proper port forwarding.
- Pipeline YAML location — By default Woodpecker looks for
.woodpecker.ymlor.woodpecker.yaml. You can override withWOODPECKER_DEFAULT_PIPELINE_PATH.
Troubleshooting
| Problem | Solution |
|---|---|
| Agent not connecting | Verify WOODPECKER_AGENT_SECRET matches on server and agent; check port 9000 is open |
| Pipeline stuck in pending | No agent is connected or agent labels don’t match pipeline labels; check agent status in UI |
| ”Cannot pull image” | Agent has no internet access or registry credentials are missing; check Docker network |
| Webhook not triggering | Verify forge can reach Woodpecker server; check webhook in Gitea repo Settings > Hooks |
| Secrets not injected | Secret name in pipeline must match exactly; check secret event filter (push/tag/pr) |
| Multi-platform build fails | Ensure agent has QEMU installed or use --privileged; check Docker Buildx is available |
Summary
- Woodpecker CI is a lightweight Drone CI fork using 50 MB RAM with server + agent architecture.
- Integrates with 6 forges (Gitea, Forgejo, GitHub, GitLab, Bitbucket, Gitea) via OAuth2.
- Pipeline syntax uses
.woodpecker.ymlwith steps, services, matrix builds, and path-based multi-pipeline. - Secrets are scoped per-repository, per-organization, or globally, with external Vault/AWS backends.
- Agents support Docker, local, and Kubernetes backends with label-based routing.
woodpecker-cliprovides full API access for pipeline management, secret administration, and cron scheduling.