Woodpecker CI is a lightweight, open source CI/CD engine that runs every pipeline step in its own container. It is a popular companion for self-hosted Forgejo and Gitea instances because it logs users in through the forge's OAuth2 provider and registers repository webhooks automatically. In this tutorial you will deploy the Woodpecker server and one agent with Docker Compose on Ubuntu 24.04, put the web interface behind Nginx with HTTPS, and run a pipeline that uses services and secrets.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with at least 2 GB of RAM, for example a CubePath VPS. Builds run on this machine, so size it for your workloads.
- A non-root user with
sudoprivileges. - Docker Engine and the Docker Compose plugin installed from Docker's official repository.
- A working Forgejo or Gitea instance reachable over HTTPS, for example
https://git.your_domain, where you have an account. - A domain for Woodpecker, such as
ci.your_domain, with a DNSArecord pointing to this server. - UFW enabled with OpenSSH allowed.
Woodpecker's Gitea integration works unchanged with Forgejo, so this guide uses the WOODPECKER_GITEA_* settings for both.
Step 1 - Creating an OAuth2 application in Forgejo
Woodpecker has no user database of its own: users sign in with their forge account. Register Woodpecker as an OAuth2 application so the forge can issue it credentials.
- Log in to Forgejo or Gitea and open Settings > Applications.
- Under Manage OAuth2 applications, set Application name to
Woodpecker CI. - Set Redirect URI to
https://ci.your_domain/authorize. - Click Create application.
Copy the Client ID and Client secret that appear. The secret is only shown once.
Step 2 - Preparing the configuration
Create a directory for the deployment:
sudo mkdir -p /opt/woodpecker
cd /opt/woodpecker
The server and the agents authenticate each other with a shared secret. Generate one:
openssl rand -hex 32
6b1f0c7d9e2a4b58c3d1e0f9a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8
Create the environment file that holds all secrets:
sudo nano /opt/woodpecker/.env
WOODPECKER_HOST=https://ci.your_domain
WOODPECKER_OPEN=false
WOODPECKER_ADMIN=your_forge_username
WOODPECKER_AGENT_SECRET=paste_the_generated_secret_here
WOODPECKER_GITEA=true
WOODPECKER_GITEA_URL=https://git.your_domain
WOODPECKER_GITEA_CLIENT=your_oauth_client_id
WOODPECKER_GITEA_SECRET=your_oauth_client_secret
What these settings do:
WOODPECKER_HOSTis the public URL. It must match the redirect URI you entered in the forge, minus/authorize.WOODPECKER_OPEN=falsestops anyone with a forge account from registering in Woodpecker. Only the users listed inWOODPECKER_ADMIN(comma separated) and users an admin adds can log in.WOODPECKER_GITEA_URLis the base URL of your Forgejo or Gitea instance.
Restrict the file permissions, since it contains secrets:
sudo chmod 600 /opt/woodpecker/.env
Step 3 - Writing the Docker Compose file
Create the compose file:
sudo nano /opt/woodpecker/compose.yaml
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:v3
container_name: woodpecker-server
env_file: .env
ports:
- "127.0.0.1:8000:8000"
volumes:
- woodpecker-server-data:/var/lib/woodpecker
restart: unless-stopped
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:v3
container_name: woodpecker-agent
command: agent
environment:
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
- WOODPECKER_MAX_WORKFLOWS=2
volumes:
- woodpecker-agent-config:/etc/woodpecker
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- woodpecker-server
restart: unless-stopped
volumes:
woodpecker-server-data:
woodpecker-agent-config:
Some notes on this layout:
- The web UI listens on port 8000, bound to
127.0.0.1so that only Nginx can reach it. - The agent talks to the server over gRPC on port 9000 through the internal Compose network, so that port is not published on the host at all.
- The server stores its SQLite database in the
woodpecker-server-datavolume, which is fine for a single server. For large teams, pointWOODPECKER_DATABASE_DRIVERandWOODPECKER_DATABASE_DATASOURCEat PostgreSQL. WOODPECKER_MAX_WORKFLOWSlimits how many workflows the agent runs in parallel.
Warningthe agent mounts the Docker socket, which gives pipelines a path to root on the host. Only enable repositories whose contributors you trust, or run agents on a dedicated server.
Start the stack:
sudo docker compose up -d
Check the logs of the agent to confirm it connected to the server. You should see a line reporting that the agent started with the docker backend and no connection errors after it:
sudo docker compose logs woodpecker-agent | tail -n 5
Repeated authentication or connection errors usually mean the agent secret does not match the server's.
Step 4 - Configuring Nginx and HTTPS
Install Nginx and Certbot:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Create a server block:
sudo nano /etc/nginx/sites-available/woodpecker
server {
listen 80;
listen [::]:80;
server_name ci.your_domain;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_buffering off;
}
}
proxy_buffering off lets the build logs stream live to the browser.
Enable the site, open the firewall and get a certificate:
sudo ln -s /etc/nginx/sites-available/woodpecker /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d ci.your_domain
Verify that the site answers over HTTPS:
curl -sI https://ci.your_domain | head -n 1
HTTP/2 200
Step 5 - Logging in and activating a repository
Open https://ci.your_domain and click Login. You are redirected to Forgejo, asked to authorize the Woodpecker CI application, and sent back to the Woodpecker dashboard.
To enable CI for a repository:
- Click Add repository (the + button).
- Find the repository in the list and click Enable.
Woodpecker creates a webhook in the repository automatically. You can confirm it in Forgejo under the repository's Settings > Webhooks, pointing to https://ci.your_domain.
Step 6 - Writing your first pipeline
Woodpecker reads pipelines from .woodpecker.yaml in the repository root, or from every file in a .woodpecker/ directory (one workflow per file). Each step runs in the image you specify, with the repository checked out in the working directory.
Add this .woodpecker.yaml to a Node.js project:
when:
- event: [push, pull_request, manual]
steps:
- name: install
image: node:22
commands:
- npm ci
- name: test
image: node:22
environment:
DATABASE_URL: postgres://app:app@database:5432/app_test
commands:
- npm test
services:
- name: database
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app_test
How it works:
- The top-level
whendecides which events start the workflow.manuallets you run it from the UI. - Steps share the workspace, so
node_modulesinstalled ininstallis available intest. servicesstart alongside the steps and are reachable by theirnameas a hostname, heredatabase. The credentials are throwaway values that only exist inside the pipeline.
Commit and push the file. The pipeline appears in the Woodpecker UI within seconds and you can follow each step's log as it runs.
Step 7 - Using secrets
Credentials such as registry passwords or deploy keys belong in Woodpecker's secret store, not in the YAML file. In the repository view in Woodpecker, open Settings > Secrets, click Add secret, and create a secret named deploy_token.
Reference it from a step with from_secret, which exposes it as an environment variable:
steps:
- name: deploy
image: alpine:3
environment:
DEPLOY_TOKEN:
from_secret: deploy_token
commands:
- test -n "$DEPLOY_TOKEN" && echo "token is available"
when:
- event: push
branch: main
Woodpecker masks secret values in the logs. The step-level when means the deploy step only runs on pushes to main, not on pull requests.
Troubleshooting
Login fails with an OAuth redirect_uri error. The redirect URI in the forge must be exactly https://ci.your_domain/authorize, and WOODPECKER_HOST must be https://ci.your_domain. After editing .env, run sudo docker compose up -d --force-recreate.
Pushes do not trigger pipelines. Open the webhook in Forgejo (Settings > Webhooks) and look at Recent deliveries. If Forgejo refuses to call the webhook because the host resolves to a private address, add the Woodpecker hostname to ALLOWED_HOST_LIST in the [webhook] section of Forgejo's app.ini and restart Forgejo.
Pipelines stay in the pending state. No agent is connected. Check sudo docker compose logs woodpecker-agent for secret mismatches or connection errors.
A step cannot resolve a service hostname. The service name in services must match the hostname used in the step, and services must be defined in the same workflow file.
Conclusion
You now have Woodpecker CI running with Docker Compose, authenticated against Forgejo or Gitea, served over HTTPS and running pipelines with services and secrets. From here you can add more agents on other machines by pointing them at the server's gRPC port with the same agent secret, move the database to PostgreSQL as usage grows, and use the official plugins such as woodpeckerci/plugin-docker-buildx to build and push container images.
