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 sudo privileges.
  • 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 DNS A record 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.

  1. Log in to Forgejo or Gitea and open Settings > Applications.
  2. Under Manage OAuth2 applications, set Application name to Woodpecker CI.
  3. Set Redirect URI to https://ci.your_domain/authorize.
  4. 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_HOST is the public URL. It must match the redirect URI you entered in the forge, minus /authorize.
  • WOODPECKER_OPEN=false stops anyone with a forge account from registering in Woodpecker. Only the users listed in WOODPECKER_ADMIN (comma separated) and users an admin adds can log in.
  • WOODPECKER_GITEA_URL is 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.1 so 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-data volume, which is fine for a single server. For large teams, point WOODPECKER_DATABASE_DRIVER and WOODPECKER_DATABASE_DATASOURCE at PostgreSQL.
  • WOODPECKER_MAX_WORKFLOWS limits how many workflows the agent runs in parallel.

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:

  1. Click Add repository (the + button).
  2. 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 when decides which events start the workflow. manual lets you run it from the UI.
  • Steps share the workspace, so node_modules installed in install is available in test.
  • services start alongside the steps and are reachable by their name as a hostname, here database. 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.