Open-source alternatives guide
Self-Host Woodpecker CI for Gitea and Forgejo 2026
Self-host Woodpecker CI in 2026. Apache 2.0, ~4K stars, Go — lightweight Drone fork purpose-built for Gitea and Forgejo. Docker Compose setup, pipeline YAML.
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
Why Self-Host Woodpecker CI
For teams already running self-hosted Gitea or Forgejo, Woodpecker CI is the natural complement. The alternatives — GitHub Actions, CircleCI, GitLab CI — all assume you're pushing code to a hosted platform. Woodpecker is designed from the ground up for the self-hosted stack.
Cost is a real consideration for CI. GitHub Actions charges $0.008/minute for Linux runners beyond the free tier. CircleCI's Performance plan starts at $15/month and limits parallelism. For an active development team running 500 minutes of CI per week, that's $200+/month on hosted CI. Woodpecker on a dedicated $20/month VPS handles that workload for a fraction of the cost, and the build speed improves because your agents are co-located with your Git server.
The architectural elegance of Woodpecker matters too. Each pipeline step runs in its own Docker container pulled fresh from a registry. There's no shared state between runs, no mysterious environment pollution, and no "works on my machine" debugging. The YAML format is intentionally simple — a new developer can read .woodpecker.yml and understand the entire build process in five minutes.
Multi-pipeline files (.woodpecker/*.yml) are a killer feature for monorepos and complex projects. You can have separate pipelines for testing, building, deploying, and scheduled maintenance tasks, each with different triggers and conditions. This is something Drone (Woodpecker's ancestor) never supported.
When NOT to self-host Woodpecker: If your team is pushing to GitHub, GitLab, or Bitbucket, Woodpecker's integration with those platforms exists but isn't its primary focus — Gitea Actions or native CI will serve you better. Also, if you need GitHub Actions compatibility (reusing the enormous marketplace of Actions), Woodpecker isn't compatible. For GitHub-hosted code, Woodpecker is the wrong choice.
Prerequisites
Woodpecker CI's server and agent are lightweight by CI standards, but the build infrastructure deserves planning.
Server specs for the Woodpecker server: 1 vCPU and 1GB RAM is sufficient for the server process itself. The server just schedules and records — it doesn't execute builds. However, if your agents run on the same machine, size appropriately for your build workloads. A Docker build for a Node.js app typically uses 1-2 vCPUs and 512MB-1GB RAM. For a small team running a few concurrent builds, a $10-15/month VPS (4 vCPUs, 4GB RAM) handles everything. See our VPS comparison for self-hosters for current pricing.
Gitea or Forgejo requirement: Woodpecker integrates with Gitea and Forgejo as its primary platforms, plus GitHub and GitLab. You need an existing OAuth2 application created in your Git forge — the setup in Part 3 walks through this. Without the OAuth2 integration, users can't authenticate.
Docker on agent machines: Agents require Docker to run pipeline steps. The agent mounts the Docker socket (/var/run/docker.sock) to launch containers. This means the agent has root-equivalent access to the host — keep agents on dedicated build machines, not on machines with sensitive data.
Operating system: Ubuntu 22.04 LTS for both server and agents. The Go binaries are statically compiled and work on any Linux, but Ubuntu gives you the best support for Docker and unattended security updates.
Skill level: Intermediate. You need to understand Docker, YAML syntax, and how webhooks work. The OAuth2 setup is the trickiest part for newcomers.
Production Security Hardening
CI systems are high-value attack targets because they have access to deployment keys, registry credentials, and production environments. Woodpecker's secrets system helps, but you need defense in depth. Follow the self-hosting security checklist and apply these Woodpecker-specific measures:
Firewall (UFW): The gRPC port (9000) only needs to be accessible from your agents. If agents are on the same machine, block it from the internet entirely.
sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Block gRPC from internet (agents connect via Docker network):
# sudo ufw deny 9000 # Only if agents are external
sudo ufw enable
Disable open registration: WOODPECKER_OPEN: "false" prevents anyone with a Gitea account from using your CI. Only users you explicitly add as admins or repo owners can use it.
Secrets management: Never put registry passwords, SSH deploy keys, or API tokens in .woodpecker.yml. Use Woodpecker secrets exclusively and reference them with from_secret. This ensures secrets are encrypted at rest in Woodpecker's database and never appear in pipeline logs.
# .env (never commit this)
AGENT_SECRET=your-very-long-random-secret
GITEA_CLIENT_SECRET=oauth-secret-here
POSTGRES_PASSWORD=db-password-here
echo ".env" >> .gitignore
Agent Docker socket security: The agent needs /var/run/docker.sock access, which is effectively root. Limit what the agent can do by running pipeline steps in containers that don't mount the Docker socket unless absolutely necessary.
Automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Backup Woodpecker data: The SQLite database (or PostgreSQL) contains your pipeline history, secrets, and configuration. Back this up so you can restore CI after a server failure. See automated server backups with restic for automated volume backups.
Pipeline Design Best Practices
How you structure your Woodpecker pipelines has a significant impact on developer experience, build speed, and infrastructure efficiency.
The multi-file pipeline feature (.woodpecker/*.yml) is your most powerful tool for keeping CI manageable as projects grow. The pattern that works well for most teams is separating pipelines by audience and frequency: a test.yml that runs on every push and pull request (fast, developer-facing), a build.yml that runs on merge to main (produces artifacts), and a deploy.yml that runs after successful builds (production-facing). This separation means a developer can get test results in 2-3 minutes without waiting for the full build-and-deploy pipeline.
Pipeline caching dramatically speeds up builds. For Node.js projects, caching node_modules between runs can reduce install time from 60 seconds to 5 seconds. Woodpecker supports volume-based caching through the volumes key in pipeline steps. Mount a named volume at your cache directory, and subsequent runs on the same agent reuse it. Be aware that caches are agent-specific — if you have multiple agents, each maintains its own cache and the first run on a new agent is always slow.
Keep steps focused on a single responsibility. A step that lints, tests, and builds is harder to debug than three separate steps. When a build fails, you want to know immediately whether it's a lint failure (solvable in seconds) or a test failure (requires investigation) without reading through interleaved output. Short step names in the UI also make the pipeline status at a glance more readable.
Secret management deserves careful thought. Woodpecker's scoped secrets (per-repo, per-org, global) solve different problems. Use repo-level secrets for things specific to that project (a deploy key, a specific API token). Use org-level secrets for shared credentials (a registry login, a shared monitoring API key). Never put the same secret in multiple places — updating it becomes a maintenance burden. If you have more than 20 secrets across your org, document them in a secrets inventory so the next engineer knows what exists.
For pull request builds, Woodpecker blocks access to secrets from fork PRs by default — this is a security feature, not a bug. Fork PRs could otherwise steal your secrets by printing them in a build step. If you need to run certain secrets in PRs (e.g., a test database), use the trusted flag carefully and only for repos where you control all contributors.
Troubleshooting Common Issues
Pipelines never start — stuck in "pending" state
Almost always an agent connectivity issue. Check that the agent is connected: UI → Administration → Agents. If no agents appear, the agent can't reach the server's gRPC port (9000). Verify the WOODPECKER_SERVER environment variable points to the correct host:port. If agent and server are in the same Docker Compose stack, use the service name (woodpecker-server:9000), not localhost.
OAuth2 redirect fails after Gitea login
The redirect URI in Gitea's OAuth2 app must exactly match https://ci.yourdomain.com/authorize. A trailing slash, HTTP vs HTTPS mismatch, or wrong domain causes an immediate rejection. Delete the OAuth2 app in Gitea, recreate it with the exact URI, and update GITEA_CLIENT_ID/GITEA_CLIENT_SECRET in your .env.
Pipeline steps fail with "Cannot connect to the Docker daemon"
The agent can't access Docker. Verify /var/run/docker.sock is mounted in the agent container: docker inspect woodpecker_agent | grep docker.sock. If it's missing, check your docker-compose.yml volumes section. Also check permissions: ls -la /var/run/docker.sock — the agent needs read/write access.
Secrets not available in pipeline steps
Secrets in Woodpecker are scoped by event type. When you add a secret, you specify which events it's available for (push, pull_request, tag, etc.). If a secret is missing in a step, check the secret's event configuration in UI → Repo → Settings → Secrets. For pull_request events from forks, secrets are withheld by default for security — this is intentional behavior.
Database locked errors (SQLite)
SQLite handles concurrent writes poorly under heavy CI load. If you're running many concurrent pipelines and seeing database errors, switch to PostgreSQL (Part 7). For small teams with sequential builds, SQLite is fine. The migration path is: stop Woodpecker, export data, spin up PostgreSQL, update environment variables, and restart the stack cleanly.
See all open source CI/CD and DevOps tools at OSSAlt.com/categories/devops.
The SaaS-to-Self-Hosted Migration Guide (Free PDF)
Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.
Join 300+ self-hosters. Unsubscribe in one click.