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.
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, notdocker-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/droneanddrone/drone-runner-docker. - Environment variable prefix is
DRONE_notWOODPECKER_. - 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-agentand 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_WORKFLOWSis set to 0. - SQLite performance under load — Switch to Postgres by adding a
postgresservice to the Compose file and settingWOODPECKER_DATABASE_DRIVER=postgresandWOODPECKER_DATABASE_DATASOURCEaccordingly. Migrate existing data with the official migration docs.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.