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:
- β Explain why Woodpecker is "container-native" and what the server/agent split buys you
- β Create a Forgejo OAuth2 application for Woodpecker
- β Generate an agent shared secret (WOODPECKER_AGENT_SECRET)
- β Deploy server + agent with Docker Compose behind Traefik
- β Set every WOODPECKER_FORGEJO_* setting correctly on the first try
- β Log in with Forgejo, activate a repo, and verify the webhook landed
- β Troubleshoot the classic install failures (redirect URI mismatch, webhook unreachable, agent never connects)
Table of Contents
- Why Woodpecker
- Architecture: Server + Agent
- Prerequisites Checklist
- Step 1: Register a Forgejo OAuth Application
- Step 2: Generate the Agent Secret
- Step 3: Docker Compose Deployment
- Step 4: First Login
- Step 5: Activate a Repository
- Troubleshooting
- Key Takeaways
- 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
βββββββββββββββββ
- Server: hosts the web UI and REST API, receives forge webhooks, stores pipeline state, queues work, and exposes a gRPC endpoint.
- Agent(s): stateless workers that dial out to the server over gRPC, pick up jobs, and execute each step as a container via the Docker socket. No inbound ports needed for agents.
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/loginwill not matchhttps://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
- In Woodpecker, go to Repositories β New repository.
- You'll see the repository list pulled from Forgejo via the API.
- Click Enable next to a repo. You must have admin rights on that repo β activation requires adding a webhook.
- Woodpecker registers a webhook at
https://ci.example.com/hookon 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
- Woodpecker = lightweight server (UI/API/webhooks/queue) + stateless agents that run every step in containers.
- Forgejo integration needs exactly four variables: enable flag, URL, OAuth client ID, OAuth secret β plus the shared agent secret.
- The redirect URI must be literally
<WOODPECKER_HOST>/login. - Activation installs a webhook; no webhook, no pipelines.
- Scaling CI is
--scale agent=N, nothing more.
Next Steps
- Write and debug your first
.woodpecker.yml: woodpecker-first-pipeline - Why this whole setup exists: manual-vs-automated
- Writing good images to build/publish: containers/dockerfile-guide
Sources & Related
Researched via headless-browser session (DuckDuckGo results were bot-challenged; official documentation fetched directly):
- https://woodpecker-ci.org/docs/intro (Welcome to Woodpecker, v3.18.x)
- https://woodpecker-ci.org/docs/usage/intro (Your first pipeline β repo activation & webhooks)
- https://woodpecker-ci.org/docs/usage/secrets (secret store levels, from_secret syntax)
- https://woodpecker-ci.org/docs/usage/matrix-workflows (matrix axes)
- Related KB: forgejo-introduction, forgejo-installation, cicd-concepts, containers/dockerfile-guide, gitops/gitops
Change Log
- 2026-08-26: Initial draft created via headless-browser web research session.