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 optionaldepends_onfor 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:v3andwoodpeckerci/woodpecker-agent:v3are 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/
.envfiles - 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) andgit.example.com(Gitea or compatible forge) - DNS
A/AAAArecords 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_SECRETwithopenssl rand -hex 32 - Prefer pinned major tags (
:v3) over floating:latestin production - Keep
.envout 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.
- Sign in to Gitea as a user who can create OAuth apps (admin for site-wide apps).
- For a user app, open
https://git.example.com/user/settings/applications. - For a system-wide app (preferred on shared forges), open
https://git.example.com/admin/settings/applications. - Create an OAuth2 application.
- Set the authorization callback URL to
https://ci.example.com/authorize— same scheme and hostname asWOODPECKER_HOST, with/authorizeas the path. - 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_HOSTmust be the public URL with scheme and no trailing slash.- The agent talks to the server’s gRPC port (
9000by default) on the Compose network. - The agent needs the Docker socket because steps run as containers on the host daemon.
- Binding
8000to127.0.0.1keeps 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.
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.
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
- Open
https://ci.example.com - Choose the Gitea login path
- Approve the OAuth application if prompted
- Confirm you land in the Woodpecker UI with your forge identity
- 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.
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.comloads with a valid certificate- OAuth login completes and returns to Woodpecker
- Callback URL mistakes are ruled out (
/authorizeexact match) - A repository can be enabled and shows a webhook on the forge
- A push runs the
hellostep green - Agent container stays healthy after reboot (
docker compose up -d) .envpermissions are600and 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.
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/authorizewith the same scheme and host asWOODPECKER_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,loopbackand restart Gitea; confirm DNS forci.example.comis reachable from the forge. - Pipelines stuck pending: The agent is down, the agent secret does not match, or the agent cannot reach
woodpecker-server:9000on 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_NETWORKper 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 — checkdocker compose logs woodpecker-server. - Users cannot register:
WOODPECKER_OPEN=falseblocks new registrations; open registration temporarily or pre-create admin access intentionally. - SELinux denials on the socket (RHEL-family hosts): Mount with
:zas 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