<!-- OSSAlt AI-readable guide source -->
<!-- Canonical: https://ossalt.com/guides/self-host-woodpecker-ci-2026 -->
<!-- Raw Markdown: https://ossalt.com/guides/self-host-woodpecker-ci-2026/raw.md -->
<!-- Source path: content/guides/self-host-woodpecker-ci-2026.mdx -->

---
og_image: "/images/guides/self-host-woodpecker-ci-2026.webp"
title: "Self-Host Woodpecker CI for Gitea and Forgejo 2026"
description: "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."
date: "2026-03-09"
author: "OSSAlt Team"
tags: ["woodpecker", "ci-cd", "devops", "self-hosting", "docker", "gitea", "forgejo", "2026"]
tier: 1
---

## TL;DR

[Woodpecker CI](https://woodpecker-ci.org) (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

| 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

```yaml
# 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:
```

```bash
# .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

```caddyfile
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

```yaml
# .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
```

```yaml
# .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
```

```yaml
# .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:

```yaml
matrix:
  NODE_VERSION:
    - 18
    - 20
    - 22

steps:
  - name: test
    image: "node:${NODE_VERSION}-alpine"
    commands:
      - npm ci
      - npm test
```

### Cron schedule

```yaml
# .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

```bash
# 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:
```yaml
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:

```yaml
# 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:
```yaml
# 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:

```yaml
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

```bash
# 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](/guides/self-hosting-vps-comparison-2026) 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](/guides/self-hosting-security-checklist-2026) 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.

```bash
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.

```bash
# .env (never commit this)
AGENT_SECRET=your-very-long-random-secret
GITEA_CLIENT_SECRET=oauth-secret-here
POSTGRES_PASSWORD=db-password-here
```

```bash
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:**

```bash
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](/guides/automated-server-backups-restic-rclone-2026) 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](https://ossalt.com).*
