Woodpecker Installation - Self-Hosted CI Wired to Forgejo

Status: Active
Last Updated: 2026-08-26
Category: CI/CD - Phase 3: Woodpecker CI
Prerequisites: forgejo-installation
Time: 1-2 hours
Tags: woodpecker, ci, cd, forgejo, docker-compose, oauth, self-hosted, pipelines

Summary

Woodpecker is a lightweight, container-native CI/CD engine that watches your Forgejo repositories and runs pipelines in throwaway containers on every push. This guide deploys the server + agent pair with Docker Compose, wires it to your existing Forgejo instance via an OAuth application and webhooks, and walks through first login and repository activation.

🎯 What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Why Woodpecker
  2. Architecture: Server + Agent
  3. Prerequisites Checklist
  4. Step 1: Register a Forgejo OAuth Application
  5. Step 2: Generate the Agent Secret
  6. Step 3: Docker Compose Deployment
  7. Step 4: First Login
  8. Step 5: Activate a Repository
  9. Troubleshooting
  10. Key Takeaways
  11. Next Steps

Why Woodpecker

Woodpecker is the community-maintained continuation of the Drone 0.x codebase β€” small enough to run happily next to Forgejo on the same box, opinionated about one thing done well: run pipeline steps inside containers, triggered by forge events.

Concern Woodpecker's answer
Execution model Every step runs in its own container image; no host pollution
Config One YAML file in the repo (.woodpecker.yml), versioned like code
Footprint Single Go binary per component; ~50–100 MB RAM each
Scaling Agents are stateless workers β€” add more, they just connect
Forge support GitHub, GitLab, Gitea, Forgejo, Bitbucket, Codeberg

Compare to Jenkins (a JVM monolith with plugin sprawl) or hosted CI (your code leaves your network): Woodpecker gives you Drone-style pipelines entirely under your control.

πŸ“– If you're new to the concepts of continuous integration vs continuous delivery, read cicd-concepts first.

Architecture: Server + Agent

Woodpecker ships as two cooperating components:

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   Forgejo ──webhook────────────────▢┐               β”‚
   (git.example.com)                β”‚   SERVER      β”‚
                                    β”‚  :8000 API/UI β”‚
        browser ──OAuth login───────▢│  :9000 gRPC   β”‚
                                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                           β”‚ gRPC (agent connects out)
                                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚    AGENT      │──▢ docker.sock
                                    β”‚ pulls jobs,   β”‚    runs step
                                    β”‚ runs steps    β”‚    containers
                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This split matters later: when builds pile up, you docker compose up -d --scale agent=3 instead of rearchitecting anything.

πŸ”’ The agent only needs outbound connectivity to the server plus access to /var/run/docker.sock. The Docker socket is root-equivalent β€” treat the agent like a privileged component (kb/containers/docker-security).

Prerequisites Checklist

# Confirm before starting:
# 1. Forgejo is already running and reachable, e.g. https://git.example.com
curl -sI https://git.example.com | head -1     # HTTP/2 200

# 2. A wildcard or dedicated DNS record for CI:
dig +short ci.example.com                      # β†’ your server IP

# 3. Traefik (or another reverse proxy) handling TLS
docker ps --format '{{.Names}}' | grep traefik

# 4. Ports free on the host:
sudo ss -tlnp | grep -E ':8000' || echo "8000 free"

You'll also need an admin account on Forgejo β€” creating OAuth applications requires admin rights.

Step 1: Register a Forgejo OAuth Application

Woodpecker uses Forgejo as its identity provider: you log into CI with your Forgejo account, and Woodpecker lists your repos via the API.

In Forgejo: Profile β†’ Settings β†’ Applications β†’ Manage OAuth2 applications β†’ Create new application

Field Value
Application name Woodpecker CI
Redirect URI https://ci.example.com/login

Forgejo shows you a Client ID and Client Secret immediately. Save both β€” the secret is shown once.

# Store them where your compose stack will read them
sudo mkdir -p /opt/woodpecker && cd /opt/woodpecker
cat > .env <<'EOF'
WOODPECKER_CLIENT_ID=<client-id-from-forgejo>
WOODPECKER_CLIENT_SECRET=<client-secret-from-forgejo>
EOF
chmod 600 .env

πŸ’‘ The redirect URI must match exactly, scheme included. http://ci.example.com/login will not match https://ci.example.com/login.

Step 2: Generate the Agent Secret

Agents authenticate to the server by presenting a shared secret. Generate a strong one:

openssl rand -hex 32 >> /opt/woodpecker/.env.tmp
echo "WOODPECKER_AGENT_SECRET=$(cat /tmp/wp-secret)" >> /opt/woodpecker/.env && rm /opt/woodpecker/.env.tmp

Simpler one-shot append:

echo "WOODPECKER_AGENT_SECRET=$(openssl rand -hex 32)" | sudo tee -a /opt/woodpecker/.env

Every agent that knows this secret can register with your server and run jobs β€” keep it out of git.

Step 3: Docker Compose Deployment

Directory layout:

/opt/woodpecker/
β”œβ”€β”€ .env                  # secrets from steps 1–2
└── docker-compose.yml
# /opt/woodpecker/docker-compose.yml
services:
  server:
    image: woodpeckerci/woodpecker-server:v3
    container_name: woodpecker-server
    restart: unless-stopped
    volumes:
      - ./data:/var/lib/woodpecker
    environment:
      # --- Forgejo integration ---
      WOODPECKER_FORGEJO: "true"
      WOODPECKER_FORGEJO_URL: https://git.example.com
      WOODPECKER_FORGEJO_CLIENT: ${WOODPECKER_CLIENT_ID}
      WOODPECKER_FORGEJO_SECRET: ${WOODPECKER_CLIENT_SECRET}
      # --- Instance identity ---
      WOODPECKER_HOST: https://ci.example.com
      # --- Agent auth ---
      WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
      # --- Optional hardening ---
      WOODPECKER_OPEN: "false"          # require org membership; flip true for signups
    networks:
      - proxy
      - internal

  agent:
    image: woodpeckerci/woodpecker-agent:v3
    container_name: woodpecker-agent
    restart: unless-stopped
    depends_on: [server]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      WOODPECKER_SERVER: server:9000
      WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
      # How many parallel workflows this agent executes:
      WOODPECKER_MAX_WORKFLOWS: "2"
    networks:
      - internal

networks:
  proxy:
    external: true     # Traefik's network
  internal:

If you use Traefik like our Forgejo stack does, add labels to the server service instead of exposing ports:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.woodpecker.rule=Host(`ci.example.com`)"
      - "traefik.http.routers.woodpecker.entrypoints=websecure"
      - "traefik.http.routers.woodpecker.tls.certresolver=le"
      - "traefik.http.services.woodpecker.loadbalancer.server.port=8000"

Without Traefik, publish the UI directly:

    ports:
      - "127.0.0.1:8000:8000"    # put nginx/caddy/TLS in front

Bring it up:

cd /opt/woodpecker
docker compose up -d
docker compose logs server --tail 30
# Look for: "server running" / grpc listening without errors
docker compose logs agent --tail 20
# Look for: agent registered / connected to server

What each FORGEJO variable does

Variable Purpose
WOODPECKER_FORGEJO=true Enables the Forgejo integration driver
WOODPECKER_FORGEJO_URL Base URL of your Forgejo instance (no trailing slash)
WOODPECKER_FORGEJO_CLIENT OAuth Client ID from Step 1
WOODPECKER_FORGEJO_SECRET OAuth Client Secret from Step 1

The URL is used both for the OAuth flow and for registering webhooks back onto your repositories β€” if Woodpecker can't reach Forgejo (or vice versa), everything downstream fails.

Step 4: First Login

Open https://ci.example.com. You'll land on a mostly empty dashboard with a Login button. Clicking it redirects you to git.example.com, where Forgejo asks you to authorize the "Woodpecker CI" application. Approve it, and you're bounced back into Woodpecker as an authenticated user.

Because your Forgejo user was the first to log in against a fresh instance, Woodpecker grants it admin rights automatically. Verify under Admin β†’ Users.

πŸ”’ Only users who are members of an organization owning the repo (or have direct access) can activate and see its pipelines. With WOODPECKER_OPEN=false, nobody else can even create an account beyond existing Forgejo users who log in.

Step 5: Activate a Repository

  1. In Woodpecker, go to Repositories β†’ New repository.
  2. You'll see the repository list pulled from Forgejo via the API.
  3. Click Enable next to a repo. You must have admin rights on that repo β€” activation requires adding a webhook.
  4. Woodpecker registers a webhook at https://ci.example.com/hook on the Forgejo repo.

Verify the webhook landed: in Forgejo open the repo β†’ Settings β†’ Webhooks β†’ you should see a hook pointing at your CI host. Use the Test delivery button; expect an HTTP 200 in the delivery log.

Nothing runs yet because there's no .woodpecker.yml in the repo β€” that's exactly what woodpecker-first-pipeline covers.

Troubleshooting

Symptom Likely cause / fix
invalid redirect uri after clicking Login Redirect URI in the Forgejo OAuth app doesn't exactly equal <WOODPECKER_HOST>/login. Check scheme and trailing path.
Login loops or oauth2: token exchange failed WOODPECKER_HOST doesn't match how you reach the UI (e.g. set to http while browsing https). Restart after fixing.
Repo list empty at activation Wrong WOODPECKER_FORGEJO_URL, or your OAuth app lacks scope β€” recreate the OAuth application and update the secret.
Webhook deliveries show connection refused / timeout Forgejo container can't resolve/reach ci.example.com. If both stacks share a Docker network, ensure DNS resolves externally too β€” webhooks come from Forgejo, so the URL must be routable from inside its container. Add to Forgejo's compose if needed: extra_hosts: ["ci.example.com:<host-ip>"].
Agent logs connection refused to server WOODPECKER_SERVER must point at server:9000 (gRPC port), not 8000, using the compose service name.
Pipeline stuck "pending" forever No agent connected (docker compose logs agent) or all agents' WOODPECKER_MAX_WORKFLOWS slots busy.
Steps fail with permission denied on docker.sock The agent container needs - /var/run/docker.sock:/var/run/docker.sock and a matching group ID (group_add: ["999"] on some distros).

Key Takeaways

Next Steps

Sources & Related

Researched via headless-browser session (DuckDuckGo results were bot-challenged; official documentation fetched directly):

Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse