TL;DR — Quick Summary

Deploy Woodpecker CI for lightweight self-hosted CI/CD. Configure pipelines, secrets, agents, and forge integration with Gitea, GitHub, and GitLab.

Woodpecker CI is a lightweight, self-hosted CI/CD system forked from Drone CI that runs container-native pipelines with minimal resource usage. This guide covers complete installation, Gitea forge integration, pipeline syntax, secrets management, agents, and a practical Go application deployment pipeline.

Prerequisites

  • Linux server with Docker and Docker Compose.
  • At least 512 MB RAM for server + agent (1 GB recommended).
  • A running Gitea, Forgejo, GitHub, or GitLab instance for OAuth.
  • Ports 8000 (HTTP) and 9000 (gRPC agent communication) open.

Step 1: Woodpecker Architecture

Woodpecker follows a server + agent model:

  • Server — Handles the web UI, API, webhook reception, and pipeline scheduling. Stores state in SQLite (default) or PostgreSQL.
  • Agent — Polls the server for work and executes pipeline steps inside containers. Multiple agents can connect to one server for parallel builds.
  • Forge — The Git hosting provider (Gitea, Forgejo, GitHub, GitLab, Bitbucket) that sends webhooks and provides OAuth authentication.
  • Pipeline — Defined in .woodpecker.yml at the repository root. Each pipeline is a set of steps executed sequentially (or in parallel) inside Docker containers.

When you push a commit, the forge sends a webhook to the Woodpecker server. The server queues a build and an agent picks it up, pulling container images and running steps in order.


Step 2: Install with Docker Compose

Create the Gitea OAuth App

In Gitea: Settings → Applications → OAuth2 Applications → Create OAuth2 Application

  • Application Name: Woodpecker CI
  • Redirect URI: http://your-woodpecker-server:8000/authorize

Copy the Client ID and Client Secret.

Docker Compose Configuration

# docker-compose.yml
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker-server
    restart: always
    ports:
      - "8000:8000"
      - "9000:9000"
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=false
      - WOODPECKER_HOST=http://your-woodpecker-server:8000
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=http://your-gitea-server:3000
      - WOODPECKER_GITEA_CLIENT=your-gitea-oauth-client-id
      - WOODPECKER_GITEA_SECRET=your-gitea-oauth-client-secret
      - WOODPECKER_AGENT_SECRET=a-strong-random-secret-for-agents

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker-agent
    restart: always
    depends_on:
      - woodpecker-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=a-strong-random-secret-for-agents
      - WOODPECKER_MAX_PROCS=2

volumes:
  woodpecker-server-data:
docker compose up -d

Access the Woodpecker UI at http://your-woodpecker-server:8000 and log in with Gitea OAuth.

Kubernetes with Helm

helm repo add woodpecker https://woodpecker-ci.org/helm/
helm repo update
helm install woodpecker woodpecker/woodpecker \
  --set server.env.WOODPECKER_GITEA=true \
  --set server.env.WOODPECKER_GITEA_URL=https://gitea.example.com \
  --set server.env.WOODPECKER_GITEA_CLIENT=CLIENT_ID \
  --set server.env.WOODPECKER_GITEA_SECRET=CLIENT_SECRET \
  --set agent.env.WOODPECKER_AGENT_SECRET=AGENT_SECRET

Step 3: Pipeline Syntax (.woodpecker.yml)

Woodpecker pipelines live in .woodpecker.yml (or a .woodpecker/ directory for multi-pipeline setups).

Basic Pipeline

# .woodpecker.yml
steps:
  - name: test
    image: golang:1.22
    commands:
      - go test ./...

  - name: build
    image: golang:1.22
    commands:
      - go build -o app .

  - name: notify
    image: plugins/slack
    settings:
      webhook:
        from_secret: slack_webhook
      channel: builds
    when:
      - status: [success, failure]

Conditional Execution with when

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

Services (Database, Redis)

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

steps:
  - name: test
    image: golang:1.22
    environment:
      DATABASE_URL: "postgres://test:test@postgres/testdb?sslmode=disable"
    commands:
      - go test -tags integration ./...

Matrix Builds

matrix:
  GO_VERSION:
    - "1.21"
    - "1.22"
  OS:
    - linux/amd64
    - linux/arm64

steps:
  - name: test
    image: "golang:${GO_VERSION}"
    commands:
      - go test ./...

Multi-Pipeline with Path Conditions

Place multiple files in .woodpecker/:

# .woodpecker/backend.yml
when:
  - path: "backend/**"

steps:
  - name: test-backend
    image: golang:1.22
    commands:
      - cd backend && go test ./...
# .woodpecker/frontend.yml
when:
  - path: "frontend/**"

steps:
  - name: test-frontend
    image: node:22
    commands:
      - cd frontend && npm ci && npm test

Step 4: Secrets Management

Woodpecker has three secret scopes:

ScopeSet byVisible to
RepositoryRepo ownerThat repository only
OrganizationOrg adminAll repos in the org
GlobalServer adminAll repositories

Add a Repository Secret

In the Woodpecker UI: Repository → Settings → Secrets → Add secret

Reference it in your pipeline:

steps:
  - name: deploy
    image: alpine
    environment:
      DEPLOY_KEY:
        from_secret: deploy_key
    commands:
      - echo "$DEPLOY_KEY" | base64 -d > /tmp/key
      - ./deploy.sh

External Secret Backends

Woodpecker supports pulling secrets from external systems:

# HashiCorp Vault backend
WOODPECKER_SECRET_SERVICE=vault
WOODPECKER_VAULT_ADDR=http://vault:8200
WOODPECKER_VAULT_TOKEN=hvs.your-vault-token

Step 5: Plugins and Custom Plugins

Woodpecker plugins are standard Docker images. Common plugins:

PluginImagePurpose
Dockerwoodpeckerci/plugin-docker-buildxBuild and push Docker images
Gitwoodpeckerci/plugin-gitClone with submodules
S3woodpeckerci/plugin-s3Upload artifacts to S3
Slackplugins/slackSend build notifications
Telegramappleboy/drone-telegramTelegram build alerts
Webhookwoodpeckerci/plugin-webhookPOST to arbitrary URLs

Writing a Custom Plugin

Any Docker image that reads PLUGIN_* environment variables and exits 0 on success is a Woodpecker plugin:

FROM alpine:3.19
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# Pipeline using custom plugin
steps:
  - name: custom-deploy
    image: your-registry/custom-deploy-plugin:latest
    settings:
      target_host:
        from_secret: deploy_host
      artifact_path: ./dist

Step 6: Agents, CLI, and Multi-Platform Builds

Agent Labels for Routing

Assign labels to agents and route pipelines accordingly:

# Agent with labels
WOODPECKER_FILTER_LABELS=platform=linux/arm64,type=build
# Pipeline targeting ARM64 agent
labels:
  platform: linux/arm64

steps:
  - name: build-arm
    image: golang:1.22
    commands:
      - GOARCH=arm64 go build -o app-arm64 .

Kubernetes Backend

The Kubernetes backend runs each pipeline step as an ephemeral pod:

WOODPECKER_BACKEND=kubernetes
WOODPECKER_BACKEND_K8S_NAMESPACE=woodpecker
WOODPECKER_BACKEND_K8S_STORAGE_CLASS=standard

woodpecker-cli

# Install
curl -sL https://github.com/woodpecker-ci/woodpecker/releases/latest/download/woodpecker-cli-linux-amd64 \
  -o /usr/local/bin/woodpecker
chmod +x /usr/local/bin/woodpecker

# Configure
export WOODPECKER_SERVER=http://your-server:8000
export WOODPECKER_TOKEN=your-api-token

# Manage pipelines
woodpecker pipeline ls owner/repo
woodpecker pipeline start owner/repo --branch main
woodpecker pipeline stop owner/repo 42

# Manage secrets
woodpecker secret add --repository owner/repo --name deploy_key --value "$(cat key.pem)"
woodpecker secret ls --repository owner/repo

# Sync repository
woodpecker repo sync

Cron Jobs (Scheduled Pipelines)

In the Woodpecker UI: Repository → Settings → Crons → Add cron

Or via CLI:

woodpecker cron add --repository owner/repo --name nightly-build \
  --expr "0 2 * * *" --branch main

Multi-Platform Builds with QEMU

steps:
  - name: build-multiplatform
    image: woodpeckerci/plugin-docker-buildx
    privileged: true
    settings:
      repo: registry.example.com/myapp
      platforms: "linux/amd64,linux/arm64"
      tags: latest
      username:
        from_secret: registry_user
      password:
        from_secret: registry_password

Woodpecker vs Alternatives

FeatureWoodpeckerDrone OSSGitea ActionsJenkinsConcourse
LicenseMITApache 2.0MITMITApache 2.0
RAM (idle)~50 MB~80 MB~100 MB~500 MB~200 MB
Pipeline formatYAMLYAMLYAML (GHA-compat)Groovy/YAMLYAML
Container-nativeYesYesYesOptionalYes
Matrix buildsYesNo (OSS)YesYesYes
Multi-pipelineYesNo (OSS)YesYesNo
Secrets backendsVault, AWSNoNoManyVault
Forge integrations6 forges4 forgesGitea/Forgejo10+GitHub
Kubernetes backendYesYesNoYesNo
Active maintenanceYesLimitedYesYesYes

Practical Example: Go Application Pipeline

A complete pipeline that tests, builds a Docker image, and deploys a Go application:

# .woodpecker.yml
steps:
  - name: test
    image: golang:1.22
    environment:
      CGO_ENABLED: "0"
    commands:
      - go vet ./...
      - go test -race -coverprofile=coverage.out ./...
      - go tool cover -func=coverage.out

  - name: build-binary
    image: golang:1.22
    environment:
      CGO_ENABLED: "0"
      GOOS: linux
      GOARCH: amd64
    commands:
      - go build -ldflags="-s -w -X main.version=${CI_COMMIT_SHA}" -o dist/app .
    when:
      - branch: [main, develop]
        event: push

  - name: build-docker
    image: woodpeckerci/plugin-docker-buildx
    settings:
      repo: registry.example.com/myapp
      tags:
        - latest
        - "${CI_COMMIT_SHA:0:8}"
      platforms: "linux/amd64,linux/arm64"
      username:
        from_secret: registry_user
      password:
        from_secret: registry_password
    when:
      - branch: main
        event: push

  - name: deploy
    image: alpine
    environment:
      DEPLOY_KEY:
        from_secret: deploy_ssh_key
      DEPLOY_HOST:
        from_secret: deploy_host
    commands:
      - apk add --no-cache openssh-client
      - echo "$DEPLOY_KEY" | base64 -d > /tmp/key && chmod 600 /tmp/key
      - ssh -i /tmp/key -o StrictHostKeyChecking=no deploy@$DEPLOY_HOST
          "docker pull registry.example.com/myapp:latest &&
           docker stop myapp || true &&
           docker run -d --name myapp --rm -p 8080:8080 registry.example.com/myapp:latest"
    when:
      - branch: main
        event: push

  - name: notify
    image: plugins/slack
    settings:
      webhook:
        from_secret: slack_webhook
      channel: deployments
      template: "Deploy of *myapp* `${CI_COMMIT_SHA:0:8}` to production: {{#success build.status}}succeeded{{else}}FAILED{{/success}}"
    when:
      - status: [success, failure]
        event: push
        branch: main

Gotchas and Edge Cases

  • Agent secret mismatch — Server and agent must share the same WOODPECKER_AGENT_SECRET. A mismatch causes the agent to silently fail to connect with no error in the UI.
  • Docker socket permissions — The agent container needs /var/run/docker.sock mounted. On some systems you must add the agent to the docker group or run chmod 666 /var/run/docker.sock.
  • Privileged steps — Steps using Docker-in-Docker or QEMU cross-compilation require privileged: true. Only use this for trusted repositories.
  • SQLite in production — The default SQLite backend is fine for small teams. Switch to PostgreSQL at WOODPECKER_DATABASE_DRIVER=postgres for 5+ concurrent users.
  • Webhook delivery — Woodpecker requires the server to be reachable from the forge. When running behind NAT, use a tunnel (Cloudflare Tunnel, ngrok) or ensure proper port forwarding.
  • Pipeline YAML location — By default Woodpecker looks for .woodpecker.yml or .woodpecker.yaml. You can override with WOODPECKER_DEFAULT_PIPELINE_PATH.

Troubleshooting

ProblemSolution
Agent not connectingVerify WOODPECKER_AGENT_SECRET matches on server and agent; check port 9000 is open
Pipeline stuck in pendingNo agent is connected or agent labels don’t match pipeline labels; check agent status in UI
”Cannot pull image”Agent has no internet access or registry credentials are missing; check Docker network
Webhook not triggeringVerify forge can reach Woodpecker server; check webhook in Gitea repo Settings > Hooks
Secrets not injectedSecret name in pipeline must match exactly; check secret event filter (push/tag/pr)
Multi-platform build failsEnsure agent has QEMU installed or use --privileged; check Docker Buildx is available

Summary

  • Woodpecker CI is a lightweight Drone CI fork using 50 MB RAM with server + agent architecture.
  • Integrates with 6 forges (Gitea, Forgejo, GitHub, GitLab, Bitbucket, Gitea) via OAuth2.
  • Pipeline syntax uses .woodpecker.yml with steps, services, matrix builds, and path-based multi-pipeline.
  • Secrets are scoped per-repository, per-organization, or globally, with external Vault/AWS backends.
  • Agents support Docker, local, and Kubernetes backends with label-based routing.
  • woodpecker-cli provides full API access for pipeline management, secret administration, and cron scheduling.