$linuxjunkies
>

Install Drone / Woodpecker CI

Deploy Woodpecker CI (or Drone) with Docker Compose, wire OAuth to Gitea or GitHub, connect agents, and run your first pipeline in under an hour.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Docker Engine 24+ with the Compose v2 plugin installed
  • A public hostname with a valid TLS certificate (HTTPS required for OAuth)
  • An OAuth application registered on Gitea or GitHub with the correct callback URL
  • Ports 80, 443, and 9000 open in your firewall or security group

Woodpecker CI is the community-maintained fork of Drone CI that picks up where Drone's open-core pivot left off. Both share the same pipeline YAML syntax and agent model, so this guide covers Woodpecker (recommended for new installs) and notes where Drone differs. You'll end up with a working server, at least one agent, OAuth wired to Gitea or GitHub, and a test pipeline.

Architecture Overview

Woodpecker runs two distinct services:

  • Server — handles the web UI, OAuth, webhook ingestion, and job scheduling. Talks to your Git forge via API.
  • Agent — polls the server for work, spawns pipeline containers (Docker backend by default), and streams logs back.

The server and agent communicate over gRPC. Agents can run on the same host or on separate machines for horizontal scale. All state lives in a database (SQLite is fine for small teams; Postgres is recommended for production).

Prerequisites

  • A host with Docker Engine 24+ and the Compose v2 plugin (docker compose, not docker-compose)
  • A public or LAN-accessible hostname with a valid TLS certificate (Woodpecker enforces HTTPS for OAuth callbacks)
  • An OAuth application registered on Gitea or GitHub
  • Ports 80, 443, and 9000 (gRPC) open in your firewall

Step 1 — Register an OAuth Application

Gitea

Go to Your Settings → Applications → Manage OAuth2 Applications. Create an application with the redirect URI set to https://ci.example.com/authorize. Copy the client ID and secret.

GitHub

Go to Settings → Developer settings → OAuth Apps → New OAuth App. Set the homepage URL to your Woodpecker domain and the callback URL to https://ci.example.com/authorize. Copy the client ID, then generate a client secret.

Step 2 — Generate a Shared Secret

The agent authenticates to the server with a pre-shared secret. Generate a cryptographically random value:

openssl rand -hex 32

Save the output; you'll use it as WOODPECKER_AGENT_SECRET on both services.

Step 3 — Write the Compose File

Create a directory for the stack and write the Compose file. Adjust image tags to the latest stable release (check GitHub releases).

mkdir -p /opt/woodpecker && cd /opt/woodpecker
cat > compose.yaml <<'EOF'
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v2
    restart: unless-stopped
    ports:
      - "8000:8000"   # HTTP (put Caddy/nginx in front for TLS)
      - "9000:9000"   # gRPC for agents
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=false
      - WOODPECKER_HOST=https://ci.example.com
      - WOODPECKER_GITEA=true                         # set false if using GitHub
      - WOODPECKER_GITEA_URL=https://git.example.com  # remove for GitHub
      - WOODPECKER_GITEA_CLIENT=${GITEA_CLIENT_ID}
      - WOODPECKER_GITEA_SECRET=${GITEA_CLIENT_SECRET}
      # For GitHub instead of Gitea, use:
      # - WOODPECKER_GITHUB=true
      # - WOODPECKER_GITHUB_CLIENT=${GITHUB_CLIENT_ID}
      # - WOODPECKER_GITHUB_SECRET=${GITHUB_CLIENT_SECRET}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_ADMIN=yourgitusername

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v2
    restart: unless-stopped
    depends_on:
      - woodpecker-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
      - WOODPECKER_MAX_WORKFLOWS=4
      - WOODPECKER_BACKEND=docker

volumes:
  woodpecker-server-data:
EOF

Store secrets in a .env file next to the Compose file so they don't live in shell history:

cat > .env <<'EOF'
GITEA_CLIENT_ID=paste-client-id-here
GITEA_CLIENT_SECRET=paste-client-secret-here
WOODPECKER_AGENT_SECRET=paste-openssl-output-here
EOF
chmod 600 .env

Step 4 — Put TLS in Front with Caddy

Woodpecker's server speaks plain HTTP on port 8000. Use a reverse proxy for TLS. Caddy is the quickest option — it provisions certificates automatically via ACME.

cat > /etc/caddy/Caddyfile <<'EOF'
ci.example.com {
    reverse_proxy localhost:8000
}
EOF
systemctl reload caddy

If you use nginx or Traefik, ensure the proxy forwards the X-Forwarded-Proto and X-Forwarded-For headers, or Woodpecker's OAuth redirect detection breaks.

Step 5 — Start the Stack

docker compose up -d
docker compose logs -f

Watch the logs for grpc agent connected — that confirms the agent registered with the server. If you see repeated connection errors, check that port 9000 is reachable between containers (or hosts if running agents externally).

Step 6 — Activate a Repository

Open https://ci.example.com, log in via OAuth (you'll be redirected to Gitea/GitHub and back), then click Add repository. Find your repo and toggle it on. Woodpecker registers a webhook on the Git forge automatically. Verify by checking Settings → Webhooks in the repo — you should see a hook pointing at https://ci.example.com/hook.

Step 7 — Write Your First Pipeline

Woodpecker reads .woodpecker.yaml (or a .woodpecker/ directory) from the repo root. Here's a minimal pipeline that lints and tests a Python project:

cat > .woodpecker.yaml <<'EOF'
steps:
  - name: lint
    image: python:3.12-slim
    commands:
      - pip install ruff --quiet
      - ruff check .

  - name: test
    image: python:3.12-slim
    commands:
      - pip install -r requirements.txt --quiet
      - python -m pytest tests/
EOF

Push the file to any branch. Woodpecker picks up the webhook, schedules the pipeline, and the agent pulls the images and runs the steps in sequence inside short-lived containers. Each step gets a fresh clone of the repo mounted at /woodpecker/src.

Step 8 — Add External Agents (Optional Scale-Out)

Run an agent on a second host by pointing it at the server's gRPC port and using the same shared secret. Nothing else changes:

docker run -d \
  --name woodpecker-agent \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e WOODPECKER_SERVER=ci.example.com:9000 \
  -e WOODPECKER_AGENT_SECRET=your-shared-secret \
  -e WOODPECKER_MAX_WORKFLOWS=2 \
  woodpeckerci/woodpecker-agent:v2

Port 9000 must be reachable from this host. Consider keeping it off the public internet by using a WireGuard tunnel or VPC-level security group rule.

Verification

After pushing a commit, go to https://ci.example.com/<org>/<repo> and confirm the pipeline appears and goes green. In the Woodpecker UI under Admin → Agents, all connected agents should show a heartbeat timestamp within the last 30 seconds.

# Confirm agent container is running and healthy
docker compose ps
docker compose logs woodpecker-agent --tail 20

Drone CI Differences

If you're deploying Drone instead of Woodpecker, the major differences are:

  • Images are drone/drone and drone/drone-runner-docker.
  • Environment variable prefix is DRONE_ not WOODPECKER_.
  • The pipeline file is .drone.yml, not .woodpecker.yaml.
  • Drone requires a license key for more than a handful of users on the open-source tier; Woodpecker has no such restriction.

Troubleshooting

  • OAuth redirect mismatch — The callback URL in your OAuth app must exactly match WOODPECKER_HOST/authorize. Trailing slashes break it.
  • Webhook 302 or 404 — Woodpecker returns 302 if the repo isn't activated. Double-check activation in the UI and confirm the webhook URL in the forge settings.
  • Agent won't connect (gRPC) — Run docker compose logs woodpecker-agent and look for TLS errors. If the server is behind a proxy, agents connect to the raw gRPC port (9000), not through the HTTPS proxy. Make sure 9000 is exposed and reachable.
  • Pipeline stuck in pending — Usually means no agents are connected. Check Admin → Agents in the UI or WOODPECKER_MAX_WORKFLOWS is set to 0.
  • SQLite performance under load — Switch to Postgres by adding a postgres service to the Compose file and setting WOODPECKER_DATABASE_DRIVER=postgres and WOODPECKER_DATABASE_DATASOURCE accordingly. Migrate existing data with the official migration docs.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Can I use SQLite in production?
SQLite works well for small teams (fewer than ~10 concurrent pipelines), but it uses file-level locking that can cause slowdowns under heavy load. Switch to Postgres for anything beyond a personal or small-team setup.
How do I pass secrets like API keys to pipelines without hard-coding them?
Use Woodpecker's built-in Secrets store: go to the repository settings, add a secret by name, then reference it in your pipeline YAML under the 'secrets' key or as an environment variable. Secrets are masked in logs.
Is Woodpecker compatible with existing Drone pipeline files?
Mostly yes. Woodpecker uses .woodpecker.yaml by default but accepts .drone.yml with only minor syntax differences. Plugins built for Drone generally work, though some Drone Enterprise-only features have no equivalent.
How do I restrict which users can log in?
Set WOODPECKER_OPEN=false (already in the example) so only users you explicitly add as admins or organization members (depending on your forge settings) can access the server. You can also whitelist specific organizations with WOODPECKER_ORGS.
Can agents use a backend other than Docker?
Yes. Woodpecker supports a Kubernetes backend (runs steps as Pods) and a local process backend for testing. Set WOODPECKER_BACKEND=kubernetes or WOODPECKER_BACKEND=local on the agent, with the appropriate additional configuration for the chosen backend.

Related guides