← All articles
DEVOPS Woodpecker CI: Self-Hosted CI/CD That Isn't Overcomp... 2026-02-09 · woodpecker · ci-cd · continuous-integration

Woodpecker CI: Self-Hosted CI/CD That Isn't Overcomplicated

DevOps 2026-02-09 woodpecker ci-cd continuous-integration gitea forgejo docker pipelines

You're self-hosting Gitea or Forgejo for your code. Now you want CI/CD — automated tests, builds, and deployments when you push code. You look at Jenkins and see a decade of accumulated complexity. You look at Gitea Actions and it's still maturing. You look at GitLab CI and realize you'd need to run all of GitLab.

Woodpecker CI is a community fork of Drone that's designed for self-hosters. It's simple, lightweight, and integrates natively with Gitea, Forgejo, GitHub, GitLab, and Bitbucket.

Why Woodpecker?

Woodpecker takes the "do one thing well" approach to CI/CD. It runs pipelines defined in YAML, executes them in Docker containers, and reports results back to your Git forge. That's it — no built-in artifact storage, no package registry, no issue tracker. Just CI/CD.

Woodpecker vs. Other Self-Hosted CI

Feature Woodpecker Gitea Actions Jenkins Drone
Setup complexity Low Built into Gitea High Low
Resource usage ~50 MB RAM Part of Gitea 500 MB+ RAM ~50 MB
Pipeline syntax Own YAML GitHub Actions Groovy/YAML Own YAML
Plugin ecosystem Good GitHub Actions compat Massive Good
Container-native Yes Yes Optional Yes
Gitea/Forgejo support Native Built-in Via plugin Native
GitHub Actions compat No Yes No No
Active development Very active Active Active Slow
License Apache 2.0 MIT MIT Mixed (was proprietary)

Choose Woodpecker if you want a simple, dedicated CI/CD server with clean YAML pipelines. Choose Gitea Actions if you're already on Gitea and want GitHub Actions compatibility. Choose Jenkins if you need enterprise features and can handle the complexity.

Setup with Docker

Server + Agent

services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    restart: unless-stopped
    ports:
      - 8000:8000
    volumes:
      - woodpecker_data:/var/lib/woodpecker
    environment:
      WOODPECKER_HOST: https://ci.yourdomain.com
      WOODPECKER_OPEN: "false"
      WOODPECKER_ADMIN: your-gitea-username
      # Gitea integration
      WOODPECKER_GITEA: "true"
      WOODPECKER_GITEA_URL: https://git.yourdomain.com
      WOODPECKER_GITEA_CLIENT: your-oauth-client-id
      WOODPECKER_GITEA_SECRET: your-oauth-client-secret
      # Agent communication
      WOODPECKER_AGENT_SECRET: a-long-random-secret

  woodpecker-agent:
    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: a-long-random-secret
      WOODPECKER_MAX_WORKFLOWS: 2

volumes:
  woodpecker_data:

Gitea/Forgejo OAuth Setup

  1. In Gitea: Settings → Applications → Create OAuth2 Application
  2. Name: Woodpecker CI
  3. Redirect URI: https://ci.yourdomain.com/authorize
  4. Copy the Client ID and Secret into the Woodpecker config

GitHub Integration

Replace the Gitea variables with:

WOODPECKER_GITHUB: "true"
WOODPECKER_GITHUB_CLIENT: your-github-oauth-client-id
WOODPECKER_GITHUB_SECRET: your-github-oauth-client-secret

Writing Pipelines

Create .woodpecker.yml (or .woodpecker/ directory) in your repository root.

Basic Pipeline

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

  - name: lint
    image: node:20-alpine
    commands:
      - npm ci
      - npm run lint

Each step runs in a fresh container. Steps run sequentially by default.

Parallel Steps

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

  - name: test
    image: node:20-alpine
    commands:
      - npm test
    depends_on:
      - install

  - name: lint
    image: node:20-alpine
    commands:
      - npm run lint
    depends_on:
      - install

test and lint run in parallel after install completes.

Multi-Pipeline

Split into files under .woodpecker/:

.woodpecker/
├── test.yml
├── lint.yml
└── deploy.yml

Each file is an independent pipeline that runs in parallel.

Conditional Steps

steps:
  - name: deploy
    image: alpine
    commands:
      - ./deploy.sh
    when:
      branch: main
      event: push

  - name: preview
    image: alpine
    commands:
      - ./deploy-preview.sh
    when:
      event: pull_request

Services (Databases, etc.)

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

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

Services are sidecar containers accessible by their name as a hostname.

Secrets

Add secrets through the Woodpecker UI (per-repo or organization-wide), then reference them:

steps:
  - name: deploy
    image: alpine
    secrets: [deploy_key, server_host]
    commands:
      - ssh -i $DEPLOY_KEY user@$SERVER_HOST "./restart.sh"
    when:
      branch: main
      event: push

Plugins

Woodpecker plugins are Docker images that accept configuration via environment variables. The ecosystem includes plugins for:

Example: Docker Build and Push

steps:
  - name: build-and-push
    image: woodpeckerci/plugin-docker-buildx
    settings:
      repo: registry.yourdomain.com/myapp
      registry: registry.yourdomain.com
      username:
        from_secret: registry_user
      password:
        from_secret: registry_password
      tags:
        - latest
        - "${CI_COMMIT_SHA:0:8}"
    when:
      branch: main
      event: push

Example: Notification

steps:
  - name: notify
    image: appleboy/drone-discord
    settings:
      webhook_id:
        from_secret: discord_webhook_id
      webhook_token:
        from_secret: discord_webhook_token
      message: "Build {{build.status}}: {{repo.name}} #{{build.number}}"
    when:
      status: [success, failure]

Scaling with Multiple Agents

Add more agents to handle concurrent builds:

# On server 2
services:
  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    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_FILTER_LABELS: "platform=linux/amd64"

Use labels to route specific pipelines to specific agents (e.g., ARM builds to a Raspberry Pi agent).

Matrix Builds

Test across multiple versions:

matrix:
  NODE_VERSION:
    - "18"
    - "20"
    - "22"

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

Cron Jobs

Woodpecker supports scheduled pipelines through its UI:

  1. Go to your repo in Woodpecker
  2. Settings → Cron
  3. Add a cron expression (e.g., 0 2 * * * for 2 AM daily)

Use when: event: cron to only run certain steps on scheduled builds:

steps:
  - name: security-scan
    image: aquasec/trivy
    commands:
      - trivy fs --severity HIGH,CRITICAL .
    when:
      event: cron
      cron: nightly-scan

Common Pitfalls

The Bottom Line

Woodpecker CI fills the "simple, self-hosted CI/CD" gap well. It's lightweight enough to run on a Raspberry Pi, flexible enough for real projects, and integrates cleanly with Gitea/Forgejo. The pipeline syntax is intuitive, the plugin ecosystem covers common use cases, and the server/agent architecture scales when you need it.

If you're self-hosting your Git forge and want CI/CD without the weight of Jenkins or the constraints of Gitea Actions, Woodpecker is the right choice.