Self-Hosting Woodpecker CI: Lightweight Continuous Integration

Most self-hosted CI/CD tools are either overkill for a homelab or painful to configure. Jenkins needs a PhD in XML. GitLab CI requires GitLab. GitHub Actions is locked to GitHub’s cloud (or a heavyweight self-hosted runner setup).

Woodpecker CI takes a different approach: a lightweight, container-native CI/CD engine that uses ~100 MB of RAM for the server and ~30 MB per agent. It’s a community fork of Drone CI (before the license change), fully open source under Apache 2.0, and used in production by Codeberg — one of the largest community Git hosting platforms.

If you run Gitea, Forgejo, GitHub, GitLab, or Bitbucket and want CI/CD without the bloat, Woodpecker is worth a serious look.

Woodpecker vs Other CI/CD Tools

FeatureWoodpecker CIDrone CIGitea ActionsJenkins
LicenseApache 2.0Source AvailableMIT (part of Gitea)MIT
RAM Usage~130 MB total~200 MBPart of Gitea1–4 GB
Config FormatYAMLYAMLGitHub Actions YAMLGroovy/XML
Container-NativePlugin-based
Multi-Forge SupportGitHub, Gitea, Forgejo, GitLab, BitbucketGitHub, Gitea, GitLab, BitbucketGitea onlyAny (plugins)
Plugin EcosystemGrowing (100+)LargeGitHub Actions compatibleMassive
Multi-AgentEnterprise onlyN/A
Kubernetes Backend
Web UIClean, modernCleanIntegrated in GiteaDated
ComplexityLowLowLow (if using Gitea)High

Choose Woodpecker if you want a standalone, lightweight CI/CD engine that works with multiple forges. Choose Gitea Actions if you’re already on Gitea and want GitHub Actions compatibility. Choose Jenkins if you need hundreds of plugins and don’t mind the resource overhead.

Prerequisites

  • A Linux server with Docker and Docker Compose installed
  • A Git forge (GitHub, Gitea, Forgejo, GitLab, or Bitbucket) — you’ll need OAuth credentials
  • A domain name pointed at your server (for webhooks and the web UI)
  • Basic familiarity with Docker Compose and Git

Setting Up Woodpecker CI with Docker Compose

Woodpecker has two main components:

  • Server — Web UI, API, webhook handler, pipeline scheduler
  • Agent — Executes pipeline steps as Docker containers

Create a directory for your setup:

mkdir -p ~/woodpecker && cd ~/woodpecker

Generate a Shared Secret

The server and agent authenticate via a shared secret. Generate one:

openssl rand -hex 32

Save this value — you’ll use it in both the server and agent configuration.

Create the Environment File

Create a .env file to store sensitive configuration:

# .env
WOODPECKER_HOST=https://ci.yourdomain.com
WOODPECKER_AGENT_SECRET=your_generated_secret_here

# GitHub OAuth (replace with your forge's settings)
WOODPECKER_GITHUB=true
WOODPECKER_GITHUB_CLIENT=your_github_oauth_client_id
WOODPECKER_GITHUB_SECRET=your_github_oauth_client_secret

# Admin user(s) — comma-separated forge usernames
WOODPECKER_ADMIN=your_github_username

Create the Docker Compose File

# docker-compose.yml
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    container_name: woodpecker-server
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=${WOODPECKER_OPEN:-false}
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_ADMIN=${WOODPECKER_ADMIN}
      # GitHub forge (swap for your forge — see Forge Configuration below)
      - WOODPECKER_GITHUB=${WOODPECKER_GITHUB:-true}
      - WOODPECKER_GITHUB_CLIENT=${WOODPECKER_GITHUB_CLIENT}
      - WOODPECKER_GITHUB_SECRET=${WOODPECKER_GITHUB_SECRET}

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    container_name: woodpecker-agent
    command: agent
    restart: unless-stopped
    depends_on:
      - woodpecker-server
    volumes:
      - woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_MAX_WORKFLOWS=2

volumes:
  woodpecker-server-data:
  woodpecker-agent-config:

Start the stack:

docker compose up -d

The Woodpecker UI is now available at http://your-server-ip:8000. Log in with your forge account to activate repositories.

Forge Configuration

Woodpecker authenticates users and receives webhooks through your Git forge. Here’s how to configure the most popular options:

GitHub

  1. Go to Settings → Developer settings → OAuth Apps → New OAuth App
  2. Set Authorization callback URL to https://ci.yourdomain.com/authorize
  3. Copy the Client ID and Client Secret into your .env
WOODPECKER_GITHUB=true
WOODPECKER_GITHUB_CLIENT=your_client_id
WOODPECKER_GITHUB_SECRET=your_client_secret

Gitea / Forgejo

  1. Go to Site Administration → Applications → Create a new OAuth2 Application (or user-level under Settings → Applications)
  2. Set Redirect URI to https://ci.yourdomain.com/authorize
WOODPECKER_GITEA=true
WOODPECKER_GITEA_URL=https://gitea.yourdomain.com
WOODPECKER_GITEA_CLIENT=your_client_id
WOODPECKER_GITEA_SECRET=your_client_secret

For Forgejo, use the same variables but replace GITEA with FORGEJO:

WOODPECKER_FORGEJO=true
WOODPECKER_FORGEJO_URL=https://forgejo.yourdomain.com
WOODPECKER_FORGEJO_CLIENT=your_client_id
WOODPECKER_FORGEJO_SECRET=your_client_secret

GitLab

  1. Go to Admin Area → Applications → New Application
  2. Set Redirect URI to https://ci.yourdomain.com/authorize
  3. Select the api scope
WOODPECKER_GITLAB=true
WOODPECKER_GITLAB_URL=https://gitlab.yourdomain.com
WOODPECKER_GITLAB_CLIENT=your_client_id
WOODPECKER_GITLAB_SECRET=your_client_secret

Your First Pipeline

Once you’ve activated a repository in the Woodpecker UI, create a .woodpecker/ directory in your repo with a workflow file:

# .woodpecker/build.yaml
when:
  - event: push
    branch: main

steps:
  - name: test
    image: node:20-alpine
    commands:
      - npm ci
      - npm test

  - name: build
    image: node:20-alpine
    commands:
      - npm run build

  - name: notify
    image: woodpeckerci/plugin-webhook
    settings:
      urls:
        from_secret: webhook_url
      content_type: application/json
    when:
      - status: [success, failure]

Push this file, and Woodpecker will automatically trigger the pipeline via the forge’s webhook.

Pipeline Syntax Highlights

  • Steps run sequentially by default within a workflow
  • Multiple workflows run in parallel — add separate YAML files to .woodpecker/
  • Services (databases, Redis) can be defined alongside steps
  • Matrix builds let you test across versions
  • Secrets are managed in the Woodpecker UI and referenced with from_secret

Adding Services

Need a database for integration tests? Add a services section:

services:
  - name: postgres
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=testdb
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test

steps:
  - name: test
    image: node:20-alpine
    environment:
      - DATABASE_URL=postgres://test:test@postgres:5432/testdb
    commands:
      - npm ci
      - npm run test:integration

Using the PostgreSQL Database Backend

For larger teams or high-volume pipelines, switch from SQLite to PostgreSQL:

# docker-compose.yml (updated)
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    container_name: woodpecker-server
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_ADMIN=${WOODPECKER_ADMIN}
      - WOODPECKER_GITHUB=${WOODPECKER_GITHUB:-true}
      - WOODPECKER_GITHUB_CLIENT=${WOODPECKER_GITHUB_CLIENT}
      - WOODPECKER_GITHUB_SECRET=${WOODPECKER_GITHUB_SECRET}
      - WOODPECKER_DATABASE_DRIVER=postgres
      - WOODPECKER_DATABASE_DATASOURCE=postgres://woodpecker:secret@postgres:5432/woodpecker?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    container_name: woodpecker-db
    restart: unless-stopped
    volumes:
      - woodpecker-db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=woodpecker
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=woodpecker
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U woodpecker"]
      interval: 10s
      timeout: 5s
      retries: 5

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    container_name: woodpecker-agent
    command: agent
    restart: unless-stopped
    depends_on:
      - woodpecker-server
    volumes:
      - woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_MAX_WORKFLOWS=2

volumes:
  woodpecker-server-data:
  woodpecker-db-data:
  woodpecker-agent-config:

Scaling with Multiple Agents

One of Woodpecker’s strengths is simple horizontal scaling. Add more agents on different machines to increase parallel build capacity:

# On a second machine
services:
  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    command: agent
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=ci.yourdomain.com:9000
      - WOODPECKER_AGENT_SECRET=same_secret_as_server
      - WOODPECKER_MAX_WORKFLOWS=4
      - WOODPECKER_GRPC_SECURE=true
      - WOODPECKER_GRPC_VERIFY=true

When connecting agents over the internet, enable gRPC TLS with WOODPECKER_GRPC_SECURE=true. Expose port 9000 on the server and configure your reverse proxy to handle gRPC traffic (see reverse proxy section below).

Reverse Proxy Configuration

Caddy

c}#w}io.Ooyndorlprueyeervcvdenkeoreermsersaed-ei_eg_npdrp.rprcoicooxf.xmyyyao{wguhoer2ondcdto:psm/ea/cciwkonoen.orncd-eopscmeetcr{kveevrre-:rs8e0tr0hv0eeri:n9t0e0r0net

Nginx

server {
    listen 80;
    server_name ci.yourdomain.com;

    location / {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;

        proxy_pass http://127.0.0.1:8000;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_buffering off;

        chunked_transfer_encoding off;
    }
}

For HTTPS with Nginx, add your SSL certificate configuration and a redirect block from port 80.

Secrets Management

Store sensitive values (API keys, deploy credentials) in Woodpecker’s built-in secrets manager:

  1. Navigate to your repository in the Woodpecker UI
  2. Go to Settings → Secrets → Add Secret
  3. Reference them in pipelines with from_secret:
steps:
  - name: deploy
    image: alpine
    environment:
      - DEPLOY_TOKEN:
          from_secret: deploy_token
    commands:
      - ./deploy.sh

Secrets can be scoped to specific events (push, pull request, tag) and images for security.

Organization-level secrets can be set via the CLI or API for values shared across multiple repositories:

docker exec woodpecker-server \
  woodpecker-cli secret add \
  --organization myorg \
  --name docker_password \
  --value "supersecret" \
  --event push --event tag

Backup and Restore

SQLite (Default)

# Backup
docker compose stop woodpecker-server
docker run --rm -v woodpecker_woodpecker-server-data:/data -v $(pwd):/backup alpine \
  tar czf /backup/woodpecker-backup-$(date +%Y%m%d).tar.gz -C /data .
docker compose start woodpecker-server

# Restore
docker compose stop woodpecker-server
docker run --rm -v woodpecker_woodpecker-server-data:/data -v $(pwd):/backup alpine \
  sh -c "rm -rf /data/* && tar xzf /backup/woodpecker-backup-20260321.tar.gz -C /data"
docker compose start woodpecker-server

PostgreSQL

# Backup
docker exec woodpecker-db pg_dump -U woodpecker woodpecker > woodpecker-db-$(date +%Y%m%d).sql

# Restore
docker exec -i woodpecker-db psql -U woodpecker woodpecker < woodpecker-db-20260321.sql

Updating Woodpecker

cd ~/woodpecker
docker compose pull
docker compose up -d

Woodpecker handles database migrations automatically on startup. Check the release notes for any breaking changes before major version upgrades.

Troubleshooting

Agent Can’t Connect to Server

  • Verify WOODPECKER_AGENT_SECRET matches on both server and agent
  • Check that port 9000 (gRPC) is accessible from the agent to the server
  • For remote agents, enable WOODPECKER_GRPC_SECURE=true and ensure your reverse proxy handles gRPC correctly
  • Check agent logs: docker logs woodpecker-agent

Webhooks Not Triggering

  • Confirm WOODPECKER_HOST matches your actual public URL (including https://)
  • Check your forge’s webhook settings — the payload URL should be https://ci.yourdomain.com/hook
  • Verify your firewall allows inbound connections from your forge

Pipeline Steps Failing with Permission Errors

  • The agent needs access to the Docker socket: mount /var/run/docker.sock
  • If using rootless Docker, configure the socket path accordingly
  • Check that pipeline images can be pulled (network access, registry auth)

Out of Disk Space

  • Old pipeline logs accumulate over time
  • Clean up old Docker images: docker system prune -af
  • Consider setting WOODPECKER_LOG_STORE_MAX_AGE to auto-prune old logs

Slow Builds

  • Increase WOODPECKER_MAX_WORKFLOWS on the agent for more parallelism
  • Add more agents on separate machines
  • Use lighter base images in your pipeline steps (Alpine variants)
  • Leverage Docker layer caching by using consistent base images

Power User Tips

  • Matrix builds — Test across multiple versions with matrix in your workflow
  • Cron pipelines — Schedule periodic builds (nightly tests, dependency checks) from the Woodpecker UI under Repository Settings → Cron
  • CLI toolwoodpecker-cli lets you manage repos, secrets, and pipelines from the terminal
  • Plugins — Browse woodpecker-ci.org/plugins for deployment, notification, and artifact plugins
  • Local backend — Run steps directly on the host instead of Docker (useful for builds that need hardware access)
  • Kubernetes backend — Scale agents across a k8s cluster for dynamic capacity
  • Badge URLs — Add build status badges to your README: https://ci.yourdomain.com/api/badges/owner/repo/status.svg
  • Conditional steps — Use when filters on events, branches, paths, and status to control execution flow

Wrapping Up

Woodpecker CI hits a sweet spot for self-hosters: genuinely lightweight, straightforward YAML configuration, multi-forge support, and easy horizontal scaling. It won’t replace GitHub Actions for complex enterprise workflows, but for running tests, building containers, and deploying your projects — it’s hard to beat the simplicity-to-power ratio.

Start with the SQLite setup, connect your forge, and push a .woodpecker/ config to any repo. You’ll have CI/CD running in under 15 minutes.

Related guides: