Skip to main content

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.

·OSSAlt Team
Share:

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/*.yml files 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

FeatureWoodpeckerDroneGitea Actions
LicenseApache 2.0Apache 2.0Built-in to Gitea
GitHub Stars~4K~31K
Forked fromDrone
GitHub Actions compatNoNoYes
Gitea/Forgejo nativeYes (primary)YesYes
Multi-pipeline filesYesNoYes
Matrix buildsYesYesYes
RAM usage~50MB~100MB
Cron triggersYesYesYes

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

  1. Gitea: User Settings → Applications → OAuth2 Applications → Create
  2. Name: Woodpecker CI
  3. Redirect URI: https://ci.yourdomain.com/authorize
  4. 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.