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.

🐾 What you'll need:
  • 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:

For GitHub: Settings → Developer Settings → OAuth Apps → New OAuth App:

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:

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:

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

PlanConcurrent buildsUse case
Starter ($7.99, 2 GB)1–2Solo dev, small repos
Pro ($15.99, 8 GB)4–6Small team, matrix tests
Premium ($35.99, 16 GB)8–12Mid-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.

🐱
OliveVPS Team

We've run Woodpecker as our primary CI for 18 months across our internal monorepo. Build minutes: unmeasured, because they're effectively free. The Pro plan with 4 concurrent workflows handles our team's 200+ daily builds without breaking a sweat.