Self-Hosting Woodpecker CI: A Complete Guide to Running an Open-Source CI/CD Platform

Self-Hosting Woodpecker CI: A Complete Guide to Running an Open-Source CI/CD Platform

Woodpecker CI is a lightweight, open-source continuous integration platform that pairs with Gitea, Forgejo, GitHub, GitLab, and Bitbucket. This guide walks through self-hosting Woodpecker on Ubuntu 24.04 LTS with Docker Compose, Gitea OAuth, HTTPS, agents, pipelines, backups, and upgrades.

· Updated · 13 min read #self-hosted #open-source #deployment #docker #vps #ci-cd #automation #go #woodpecker

 Self-hosted Woodpecker CI platform

Caption: Self-hosted Woodpecker CI runs a server and agent stack that connects to your forge over OAuth and executes pipelines in Docker containers.

Introduction

Woodpecker CI is an open-source continuous integration and delivery system designed to stay small, readable, and forge-friendly. Instead of bundling a full DevOps suite, it focuses on what most self-hosters need: receive a webhook when code changes, run containerized steps defined in a repository YAML file, and report status back to the forge. That model fits teams who already run Gitea, Forgejo, or another Git host and want CI without the resource cost of a heavyweight platform.

Self-hosting Woodpecker CI makes sense when build logs, secrets, and runner capacity should stay on your VPS or private network. Agencies, product teams, and homelabs often want pipelines that clone private repos, run tests, and publish artifacts without sending source to a SaaS CI product. Woodpecker keeps a server for the UI, API, and state, plus one or more agents that execute jobs through the Docker backend.

This guide installs Woodpecker CI on Ubuntu 24.04 LTS using the official Docker Compose pattern (server v3 and agent v3 images). The example pairs Woodpecker with a self-hosted Gitea forge at https://git.example.com, publishes Woodpecker at https://ci.example.com, stores SQLite state in a named volume, and covers OAuth, HTTPS, a first .woodpecker.yaml pipeline, troubleshooting, backups, and upgrades. Replace domains, secrets, and forge usernames before production use.

Why Choose Woodpecker CI?

  • Lightweight footprint: A server plus agent stack is far smaller than a full DevOps appliance, which helps on modest VPS plans.
  • Forge-native auth: Login and repository access go through OAuth with Gitea, Forgejo, GitHub, GitLab, or Bitbucket instead of inventing another user database.
  • Clear pipeline files: Workflows live in .woodpecker.yaml (or .woodpecker/*.yaml) next to your code, with serial steps and optional depends_on for fan-out.
  • Docker-first agents: Each step runs in a container image you choose; the agent mounts the host Docker socket to spawn those containers.
  • Official Compose docs: woodpeckerci/woodpecker-server:v3 and woodpeckerci/woodpecker-agent:v3 are the documented deployment path.
  • SQLite by default: Small installs can persist users, repos, and logs without standing up a separate database on day one.
  • Agent secret model: Server and agents share WOODPECKER_AGENT_SECRET, so you can add more agents later without rewriting the UI stack.

Treat Woodpecker CI as infrastructure that can execute arbitrary code from repositories you enable. A leaked agent secret, an overly open registration policy, or an agent on the same host as production secrets can turn a convenience pipeline into a privilege problem. Plan isolation before you enable every org repo.

Prerequisites

Hardware Recommendations:

  • Comfortable single-node lab: 2 vCPU and 2–4 GB RAM for server + agent on light workloads
  • Busier teams: 4+ vCPU and 8 GB RAM, especially if agents build large images or run many parallel workflows
  • 20 GB+ free SSD for OS, Docker images, build caches, and the Woodpecker data volume
  • Off-server backup capacity for the server volume and your Compose/.env files
  • Optional second host for agents when untrusted pipelines should not share the forge’s Docker daemon

Software and Accounts:

  • Ubuntu 24.04 LTS with sudo access
  • Public hostnames such as ci.example.com (Woodpecker) and git.example.com (Gitea or compatible forge)
  • DNS A/AAAA records pointing at the server (or load balancer)
  • Docker Engine with the Compose v2 plugin
  • A working forge with permission to create an OAuth application
  • OpenSSL, curl, and a password manager
  • Optional reverse proxy (Caddy, Traefik, or nginx) for TLS

Security Notes:

  • Generate a long WOODPECKER_AGENT_SECRET with openssl rand -hex 32
  • Prefer pinned major tags (:v3) over floating :latest in production
  • Keep .env out of public Git remotes
  • Do not mount the Docker socket on a host that also stores unrelated production credentials if pipelines are untrusted
  • Open only SSH (admin), HTTP, and HTTPS on the public firewall; keep gRPC between server and agent on the private Compose network when possible

Patch the host and set a tight firewall:

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Installation Guide

This deployment follows the official Docker Compose approach: register a Gitea OAuth app, write .env and compose.yaml, start server + agent, place HTTPS in front of port 8000, then enable a repository and run a test pipeline.

1. Install Docker Engine

sudo apt-get update -qqy
sudo apt-get install ca-certificates curl -qqy
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update -qqy
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -qqy

sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

2. Register a Gitea OAuth Application

Woodpecker authenticates users through the forge. On Gitea, create an OAuth2 application so the callback matches your Woodpecker public URL exactly.

  1. Sign in to Gitea as a user who can create OAuth apps (admin for site-wide apps).
  2. For a user app, open https://git.example.com/user/settings/applications.
  3. For a system-wide app (preferred on shared forges), open https://git.example.com/admin/settings/applications.
  4. Create an OAuth2 application.
  5. Set the authorization callback URL to https://ci.example.com/authorize — same scheme and hostname as WOODPECKER_HOST, with /authorize as the path.
  6. Copy the client ID and client secret into your password manager.

If Woodpecker and Gitea share one host, allow local webhook delivery in Gitea’s app.ini (required since Gitea v1.16 for loopback webhooks):

[webhook]
ALLOWED_HOST_LIST=external,loopback

Restart Gitea after editing app.ini. Also keep Gitea’s API max page size at least 50 so Woodpecker pagination keeps working.

3. Create the Compose project and secrets

sudo mkdir -p /opt/woodpecker-ci
sudo chown "$USER":"$USER" /opt/woodpecker-ci
cd /opt/woodpecker-ci

AGENT_SECRET="$(openssl rand -hex 32)"
printf 'WOODPECKER_HOST=https://ci.example.com\n' > .env
printf 'WOODPECKER_AGENT_SECRET=%s\n' "$AGENT_SECRET" >> .env
printf 'WOODPECKER_GITEA_URL=https://git.example.com\n' >> .env
printf 'WOODPECKER_GITEA_CLIENT=%s\n' 'YOUR_GITEA_CLIENT_ID' >> .env
printf 'WOODPECKER_GITEA_SECRET=%s\n' 'YOUR_GITEA_CLIENT_SECRET' >> .env
chmod 600 .env

Replace the Gitea client placeholders with the real OAuth values. Do not commit .env.

4. Write docker-compose.yaml

Create /opt/woodpecker-ci/docker-compose.yaml based on the official Compose example, adapted for Gitea:

services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    restart: always
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=true
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=${WOODPECKER_GITEA_URL}
      - WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
      - WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      # Optional: comma-separated forge usernames that become admins
      # - WOODPECKER_ADMIN=your-gitea-username

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    command: agent
    restart: always
    depends_on:
      - woodpecker-server
    volumes:
      - woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}

volumes:
  woodpecker-server-data:
  woodpecker-agent-config:

Notes that match the official docs:

  • WOODPECKER_HOST must be the public URL with scheme and no trailing slash.
  • The agent talks to the server’s gRPC port (9000 by default) on the Compose network.
  • The agent needs the Docker socket because steps run as containers on the host daemon.
  • Binding 8000 to 127.0.0.1 keeps the UI behind your reverse proxy.

If Gitea also runs in Docker on the same host and clones fail from inside pipeline containers, put the agent on Gitea’s Docker network and set WOODPECKER_BACKEND_DOCKER_NETWORK to that network name, as documented for same-host Gitea setups.

 Woodpecker CI Docker Compose topology

Caption: Woodpecker server persists SQLite under /var/lib/woodpecker; the agent mounts the Docker socket and connects to gRPC on port 9000.

5. Start the stack

cd /opt/woodpecker-ci
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f woodpecker-server

Confirm both woodpecker-server and woodpecker-agent are running. Leave the log follow with Ctrl+C once the server stays up without crash loops.

Configuration

HTTPS with a reverse proxy

Point ci.example.com at the host, then terminate TLS on Caddy, Traefik, or nginx and proxy to 127.0.0.1:8000. Example Caddy site block:

# /etc/caddy/Caddyfile (snippet)
ci.example.com {
        reverse_proxy 127.0.0.1:8000
}

Reload Caddy (or your chosen proxy) and verify:

curl -I https://ci.example.com

You should see an HTTP success response without certificate warnings. Forge webhooks and OAuth redirects must use the same public hostname.

 HTTPS path for Woodpecker CI

Caption: Terminate TLS at the reverse proxy, proxy to loopback :8000, and keep agent gRPC on the private Compose network.

Registration and admin access

With WOODPECKER_OPEN=true, users who can OAuth through Gitea can register when they first log in. For a private team forge, set WOODPECKER_OPEN=false after your admins exist, or restrict who may authenticate at the forge layer. Use WOODPECKER_ADMIN with your Gitea username(s) if you want explicit admin promotion from env.

Agent authentication

Server and agent must share the same WOODPECKER_AGENT_SECRET. Rotate it only if you can update every agent and accept that existing agent registrations need to reconnect. For agents that reach the server over the internet, enable gRPC TLS with WOODPECKER_GRPC_SECURE=true (and verification settings) as described in the Compose documentation.

Usage

First login

  1. Open https://ci.example.com
  2. Choose the Gitea login path
  3. Approve the OAuth application if prompted
  4. Confirm you land in the Woodpecker UI with your forge identity
  5. Open repository settings and enable the repo you want to build

Woodpecker creates the forge webhook when you enable a repository. Push an empty commit or open a pull request after the workflow file exists.

Add a pipeline file

Create .woodpecker.yaml in the repository root (Woodpecker also supports a .woodpecker/ directory with multiple workflow files):

when:
  - event: [push, pull_request]

steps:
  - name: hello
    image: alpine:3.20
    commands:
      - echo "Woodpecker CI is working"
      - uname -a

  - name: test
    image: alpine:3.20
    commands:
      - echo "second step sees the same workspace"
      - ls -la

Steps run in order by default. A non-zero exit fails the pipeline immediately unless you use a failure-only step condition. Skip CI for a single commit by including [CI SKIP] or [SKIP CI] in the commit message.

 From push to pipeline status

Caption: Forge webhooks hit the Woodpecker server; agents execute container steps and report status back to the forge.

Secrets and protected values

Store tokens in the Woodpecker UI (repository or org secrets) instead of committing them. Reference secrets from step environment or plugin settings using the names Woodpecker documents for secret injection. Rotate forge tokens and registry passwords on the same schedule as other production credentials.

Testing checklist

  • https://ci.example.com loads with a valid certificate
  • OAuth login completes and returns to Woodpecker
  • Callback URL mistakes are ruled out (/authorize exact match)
  • A repository can be enabled and shows a webhook on the forge
  • A push runs the hello step green
  • Agent container stays healthy after reboot (docker compose up -d)
  • .env permissions are 600 and not in git
  • Optional: second agent on another host can join with the same secret

Screenshots and Visuals

The visuals in this guide are original architecture diagrams rather than scraped product UI. They show the Compose topology, HTTPS placement, push-to-status flow, and the backup path you should rehearse before CI holds production release credentials.

 Woodpecker CI backup and upgrade workflow

Caption: Useful Woodpecker backups include the server data volume plus Compose and .env, an off-server copy, a restore drill, and a pinned image upgrade.

Troubleshooting

  • OAuth redirect errors: The Gitea callback must be exactly https://ci.example.com/authorize with the same scheme and host as WOODPECKER_HOST (no trailing slash on the host env value).
  • Login works but webhooks never fire: On same-host Gitea, set [webhook] ALLOWED_HOST_LIST=external,loopback and restart Gitea; confirm DNS for ci.example.com is reachable from the forge.
  • Pipelines stuck pending: The agent is down, the agent secret does not match, or the agent cannot reach woodpecker-server:9000 on the Compose network.
  • Clone failures inside steps: The agent cannot reach the forge URL Gitea advertises. Join the Gitea Docker network and/or set WOODPECKER_BACKEND_DOCKER_NETWORK per the Gitea forge docs.
  • Permission errors talking to Docker: The agent cannot use /var/run/docker.sock. Check the volume mount and that Docker is running on the agent host.
  • 502 from the reverse proxy: Server not listening on 127.0.0.1:8000, proxy misconfigured, or containers still restarting — check docker compose logs woodpecker-server.
  • Users cannot register: WOODPECKER_OPEN=false blocks new registrations; open registration temporarily or pre-create admin access intentionally.
  • SELinux denials on the socket (RHEL-family hosts): Mount with :z as shown in Woodpecker’s SELinux notes: /var/run/docker.sock:/var/run/docker.sock:z.

Scaling, Securing, and Next Steps

Back up the Woodpecker server volume (SQLite and related state under /var/lib/woodpecker) together with /opt/woodpecker-ci/docker-compose.yaml and .env:

cd /opt/woodpecker-ci
docker compose stop woodpecker-agent

sudo mkdir -p /var/backups/woodpecker-ci
# Copy the named volume data (adjust volume name if Compose prefixed it)
docker run --rm \
  -v woodpecker-ci_woodpecker-server-data:/data:ro \
  -v /var/backups/woodpecker-ci:/backup \
  alpine:3.20 \
  sh -c 'cd /data && tar czf /backup/woodpecker-server-data.tgz .'

cp docker-compose.yaml .env /var/backups/woodpecker-ci/
rsync -a /var/backups/woodpecker-ci/ backup-user@backup.example.net:/srv/backups/woodpecker-ci/

docker compose start woodpecker-agent

Schedule that flow with cron or a systemd timer. Test a restore on a spare VPS: restore the volume archive, restore Compose and .env, docker compose up -d, log in via OAuth, enable a repo, and run the hello pipeline.

For upgrades, read Woodpecker’s release notes, take a fresh backup, keep the :v3 major pin (or move deliberately), then:

cd /opt/woodpecker-ci
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Add capacity by running more agents (same WOODPECKER_AGENT_SECRET, pointing WOODPECKER_SERVER at the reachable gRPC endpoint). Move untrusted pipelines to a dedicated agent host so build containers never share a Docker daemon with your forge data volumes.

The outcome of this guide is a private, HTTPS-secured Woodpecker CI instance paired with Gitea OAuth, persistent server state, an agent that can run containerized steps from .woodpecker.yaml, and a backup path that includes both the data volume and env secrets. From here, tighten registration, store deploy credentials as Woodpecker secrets, isolate agents, document the restore drill, and track image tags so CI stays rebuildable when the next v3 release lands.

Need this done on your server?

I deploy and harden Laravel, CodeCanyon, and open-source apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.