GitHub Actions runs your CI for free until it doesn't — 2,000 free minutes per month for private repos disappears fast on multi-platform builds, then you pay $0.008 per minute. Woodpecker CI is the self-hosted answer — the actively-maintained fork of Drone CI, with YAML pipelines that look familiar if you've used GitLab CI or GitHub Actions, Docker-native step isolation, and zero per-minute pricing forever.
This guide installs Woodpecker server and a local agent on a single VPS, wires it up to a Gitea (or GitHub / GitLab) instance, walks through your first pipeline, covers secrets management for production use, and gets into the operational pieces — parallel builds, matrix testing, caching strategies for fast incremental builds.
- A Linux VPS — Ubuntu 22.04 or Debian 12, 2 GB RAM minimum (4 GB recommended)
- A domain pointed at your VPS
- A Gitea, GitHub, GitLab, or Bitbucket account to connect
- Roughly 35 minutes
For Woodpecker pre-installed with a runner agent already wired up, see our CI/CD VPS plans — from $7.99/mo.
1. When self-hosted CI actually pays off
Quick math: GitHub Actions free tier gives 2,000 minutes/month for private repos. A typical Node.js project (install deps + lint + test + build) takes 4 minutes per push. At 5 pushes/day per developer × 4 developers × 20 working days, you're at 1,600 minutes/month — close to the cap.
Above the cap: $0.008/minute on Linux runners ($0.064 on macOS, $0.016 on Windows). Plus storage and data egress. For a 10-developer team running active CI, expect $40–$200/month on GitHub Actions alone.
Woodpecker on a $7.99 VPS handles unlimited minutes. The break-even is fast — somewhere around 10,000 build minutes/month, self-hosted wins on cost. Above that you also get full control of the build environment, native Docker caching, and no surprise rate limits.
2. Prepare the VPS
apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg ufw
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
adduser ci
usermod -aG sudo ci
rsync --archive --chown=ci:ci ~/.ssh /home/ci
3. Install Docker
Woodpecker is Docker-native — every pipeline step runs in a fresh container. So Docker is non-optional:
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
usermod -aG docker ci
4. Create an OAuth app on your Git host
Woodpecker uses OAuth to talk to your Git server. The exact steps differ per provider:
For Gitea: Site Administration → Integrations → OAuth2 Applications → New Application:
- Name: Woodpecker CI
- Redirect URI:
https://ci.example.com/authorize
For GitHub: Settings → Developer Settings → OAuth Apps → New OAuth App:
- Application name: Woodpecker
- Homepage URL:
https://ci.example.com - Authorization callback URL:
https://ci.example.com/authorize
For GitLab: Preferences → Applications → Add new application, with the same callback URL pattern.
After creating, you get a Client ID and Client Secret. Copy both — you'll paste them in the next step.
5. Deploy Woodpecker server + agent
Switch to the ci user:
su - ci
mkdir -p ~/woodpecker
cd ~/woodpecker
# Generate a shared agent secret
AGENT_SECRET=$(openssl rand -hex 32)
echo $AGENT_SECRET > ~/.woodpecker-agent-secret
chmod 600 ~/.woodpecker-agent-secret
Create the compose file (this example uses Gitea — adjust the WOODPECKER_GITEA_* vars for your provider):
cat > ~/woodpecker/compose.yaml <<'EOF'
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000" # web UI
- "9000:9000" # agent gRPC port
volumes:
- ./server-data:/var/lib/woodpecker
environment:
WOODPECKER_OPEN: "true"
WOODPECKER_HOST: https://ci.example.com
WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: https://git.example.com
WOODPECKER_GITEA_CLIENT: PASTE_OAUTH_CLIENT_ID
WOODPECKER_GITEA_SECRET: PASTE_OAUTH_CLIENT_SECRET
WOODPECKER_AGENT_SECRET: PASTE_AGENT_SECRET_HERE
WOODPECKER_ADMIN: yourgiteausername
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: unless-stopped
depends_on:
- woodpecker-server
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./agent-config:/etc/woodpecker
environment:
WOODPECKER_SERVER: woodpecker-server:9000
WOODPECKER_AGENT_SECRET: PASTE_AGENT_SECRET_HERE
WOODPECKER_MAX_WORKFLOWS: 4
EOF
Replace the placeholders with real values, then launch:
docker compose up -d
docker compose logs -f
6. Nginx reverse proxy + HTTPS
sudo apt install -y nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/woodpecker <<'EOF'
server {
listen 80;
server_name ci.example.com;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s; # build logs stream over WebSocket
}
}
EOF
sudo ln -s /etc/nginx/sites-available/woodpecker /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d ci.example.com
Visit https://ci.example.com — click Login with Gitea/GitHub/GitLab, approve the OAuth scope, and you're in. The Woodpecker dashboard lists every repo from your Git provider with a toggle to enable CI.
7. Your first pipeline
Pick a repo from the dashboard and enable Woodpecker. This adds a webhook to the repo (you'll see it in the repo's webhook settings).
Create .woodpecker.yaml at your repo root:
when:
- event: push
branch: main
- event: pull_request
steps:
install:
image: node:20-alpine
commands:
- npm ci
lint:
image: node:20-alpine
commands:
- npm run lint
test:
image: node:20-alpine
commands:
- npm test
build:
image: node:20-alpine
commands:
- npm run build
when:
- branch: main
- event: push
Commit, push. The webhook fires, Woodpecker queues a build, the agent picks it up and runs each step in a fresh container. Watch the dashboard — each step turns green as it passes.
Pipeline syntax is similar to Drone CI (Woodpecker is the fork) and conceptually close to GitLab CI / GitHub Actions. The key idea: every step is an image + commands. No magic, no proprietary actions.
🐾 Skip the OAuth dance
Our CI/CD VPS plans ship with Woodpecker server + agent pre-configured. Just connect your Git host and start building. From $7.99/mo.
See CI/CD VPS Plans →8. Secrets and parallel builds
Pipelines need API keys, deploy tokens, registry credentials. Woodpecker has scoped secret stores:
- Repo secrets — only available to one repo's pipelines
- Org secrets — shared across all repos under an organization
- Global secrets — admin-managed, available to any pipeline
Add a secret in the Woodpecker UI under the repo's Settings → Secrets. Use it in your pipeline:
steps:
deploy:
image: alpine
commands:
- apk add curl
- curl -X POST -H "Authorization: Bearer $${API_TOKEN}" https://api.example.com/deploy
environment:
API_TOKEN:
from_secret: api_token
Parallel steps: by default Woodpecker runs steps sequentially. Mark them as parallel by giving them no dependencies and naming them under a parallel block:
steps:
parallel:
lint:
image: node:20
commands: [npm run lint]
test:
image: node:20
commands: [npm test]
typecheck:
image: node:20
commands: [npm run typecheck]
# then the rest sequentially:
build:
image: node:20
commands: [npm run build]
9. Caching for fast incremental builds
Without caching, every build reinstalls dependencies. For a Node project that's 30–90 seconds wasted per build. Woodpecker has two caching approaches:
Built-in volume cache: persist a host path across builds:
steps:
install:
image: node:20-alpine
commands:
- npm ci
volumes:
- /var/cache/npm:/root/.npm
This requires the agent to allow host volume mounts (off by default for security). Enable with WOODPECKER_AGENT_ALLOW_VOLUMES=true in the agent's environment.
External cache via S3/MinIO: more portable, works across multiple agents:
steps:
restore-cache:
image: meltwater/drone-cache:latest
settings:
backend: s3
restore: true
bucket: ci-cache
region: us-east-1
mount:
- node_modules
access_key:
from_secret: s3_access_key
secret_key:
from_secret: s3_secret_key
install:
image: node:20
commands: [npm ci]
save-cache:
image: meltwater/drone-cache:latest
settings:
backend: s3
rebuild: true
bucket: ci-cache
region: us-east-1
mount:
- node_modules
Cache hits make subsequent builds 5–10× faster.
10. Reference: scaling and integrations
Multiple agents
For parallel-build capacity, add agents — each is a separate Docker container that connects to the server via gRPC. Add to compose.yaml:
woodpecker-agent-2:
image: woodpeckerci/woodpecker-agent:latest
restart: unless-stopped
depends_on: [woodpecker-server]
volumes: [/var/run/docker.sock:/var/run/docker.sock]
environment:
WOODPECKER_SERVER: woodpecker-server:9000
WOODPECKER_AGENT_SECRET: SAME_AGENT_SECRET
WOODPECKER_MAX_WORKFLOWS: 4
Or run agents on separate VPS for true parallel capacity. Each agent processes one workflow at a time (or N if WOODPECKER_MAX_WORKFLOWS=N). Scaling out across multiple VPS is the standard pattern for serious CI loads.
Plugin ecosystem
Woodpecker is Drone-CI-compatible at the plugin level — 1,000+ existing Drone plugins work. Common ones:
- plugins/docker — build and push Docker images
- plugins/ssh — SSH into a server and run commands (deploy)
- plugins/slack — notify a Slack channel on success/failure
- plugins/s3 — upload artifacts to S3
Matrix builds
matrix:
NODE_VERSION:
- 18
- 20
- 22
steps:
test:
image: node:$${NODE_VERSION}
commands: [npm ci, npm test]
This runs the entire pipeline 3× in parallel, once per Node version. Output is grouped by matrix combination in the UI.
Per-plan capacity
| Plan | Concurrent builds | Use case |
|---|---|---|
| Starter ($7.99, 2 GB) | 1–2 | Solo dev, small repos |
| Pro ($15.99, 8 GB) | 4–6 | Small team, matrix tests |
| Premium ($35.99, 16 GB) | 8–12 | Mid-size team, big monorepos |
For larger setups, scale horizontally — multiple agent VPS, one server VPS, shared cache backend.
Integration with Gitea Actions
If you're running Gitea, you have two CI options: Gitea Actions (built in, GitHub-Actions-compatible) and Woodpecker (more powerful, separate). Use Gitea Actions for simple workflows; use Woodpecker when you need matrix builds, parallel pipelines, or sophisticated caching. They can coexist.
FAQ
Woodpecker vs Jenkins vs Drone CI?
Woodpecker is YAML-first and Docker-native — modern, lightweight. Jenkins is plugin-first and JVM-heavy — customizable to extreme degrees but operationally heavy. Drone CI is the original (Woodpecker is its actively-maintained fork) — Drone's gone enterprise-focused, Woodpecker is the community-led continuation. For modern container workflows, Woodpecker is simpler. For complex Java/legacy environments, Jenkins still rules.
Can I use Woodpecker with GitHub-hosted repos?
Yes — Woodpecker supports GitHub, GitLab, Bitbucket, and Gitea as source-of-truth. Configure the appropriate WOODPECKER_GITHUB_* (or GITLAB, BITBUCKET) env vars in compose.yaml instead of the GITEA ones. The OAuth app setup steps differ per provider but the flow is identical.
How fast are typical builds compared to GitHub Actions?
Comparable to slightly faster on dedicated cores. GitHub-hosted runners use shared infrastructure with variable performance; a dedicated-core VPS gives you consistent throughput. With volume caching enabled, repeated builds on the same agent are often 3–5× faster than cold GitHub Actions runs.
Can I use the same VPS for both Gitea and Woodpecker?
Yes — on a Pro plan ($15.99, 8 GB RAM) both fit comfortably. Gitea uses ~250 MB idle, Woodpecker server uses ~150 MB, each running build container spins up an additional image. For active small teams, this combined setup works well and minimizes infrastructure. Separate them onto two VPS when CI builds start consistently hitting RAM limits.
Does Woodpecker work with monorepos?
Yes — Woodpecker has path-based filters: when: path: ["frontend/**"]. Combined with multiple .woodpecker/ sub-pipelines, monorepos get build pipelines that only run for affected paths. Faster builds and clearer per-package CI results.
Is migrating from Drone CI straightforward?
Yes — Woodpecker forked from Drone and kept the YAML config syntax largely compatible. Most Drone 0.8 / 1.x pipelines run on Woodpecker with minor adjustments (mainly the pipeline: block renamed to steps:). Drone plugins work as-is via the same Docker image references.