Continuous integration and delivery pipelines are the backbone of any team that ships software reliably. Woodpecker CI is a lightweight, open-source CI/CD engine — a community fork of Drone CI — that pairs perfectly with self-hosted Git servers such as Forgejo and Gitea. Unlike heavyweight platforms, Woodpecker has a small footprint, runs entirely inside Docker, uses a declarative YAML pipeline syntax, and exposes a clean API for integration with the rest of your stack.
In this guide, you will build a production-oriented Woodpecker CI deployment using Docker Compose, terminate HTTPS through Caddy, back the server with a dedicated PostgreSQL database, and harden the setup with proper secret handling and operational controls. By the end, your team will have a fully functional CI server accessible at a custom domain with automated TLS, persistent state, and runbook-ready recovery paths.
Architecture and flow overview
The deployment has four layers. First, Caddy runs on the host as the public HTTPS edge, handling certificate provisioning via ACME and routing inbound traffic to Woodpecker Server on port 8000. Second, the Woodpecker Server container holds the web UI, API, and pipeline scheduler. Third, one or more Woodpecker Agent containers pull jobs from the server, execute pipeline steps inside ephemeral Docker containers, and report results back. Fourth, PostgreSQL persists pipeline runs, logs, secrets, and user records.
Agents communicate with the server over gRPC using a shared secret. The server and agents share the Docker socket only on the agent side, which keeps the attack surface minimal. Caddy handles all external TLS so neither the server nor the agents need certificate management logic. This is a clean, horizontally scalable pattern: you can add more agent containers later without touching the server or Caddy configuration.
Prerequisites
- A Linux host (Ubuntu 22.04 LTS or later) with Docker Engine and Docker Compose plugin installed.
- A DNS A record for your chosen domain (e.g.,
ci.example.com) pointing to the server's public IP. - Caddy installed on the host via the official package repository.
- A running Forgejo or Gitea instance that Woodpecker will use for OAuth login and repository access.
- Ports 80 and 443 open on the host firewall for Caddy, and port 9000 open internally for gRPC between agents and server.
- A shell user with sudo privileges.
Step-by-step deployment
1) Create the project directory
Isolating all stack files under a single directory simplifies backups and upgrades.
sudo mkdir -p /opt/woodpecker/{postgres-data,woodpecker-data}
sudo chown -R $USER:$USER /opt/woodpecker
cd /opt/woodpeckerIf the copy button does not work in your browser, manually copy the command block above.
2) Create the secrets file
Store sensitive values in a dedicated env file outside version control. Generate a strong agent secret and a strong PostgreSQL password before proceeding.
cat > /opt/woodpecker/.env << 'EOF'
WOODPECKER_AGENT_SECRET=replace_with_64_char_random_hex
POSTGRES_PASSWORD=replace_with_strong_db_password
FORGEJO_CLIENT_ID=replace_with_oauth_client_id
FORGEJO_CLIENT_SECRET=replace_with_oauth_client_secret
EOF
chmod 600 /opt/woodpecker/.envIf the copy button does not work in your browser, manually copy the command block above.
3) Write the Docker Compose file
The compose file defines the server, one agent, and PostgreSQL. The agent binds the Docker socket so it can launch pipeline containers on the host.
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: unless-stopped
env_file: .env
environment:
- WOODPECKER_OPEN=false
- WOODPECKER_HOST=https://ci.example.com
- WOODPECKER_FORGEJO=true
- WOODPECKER_FORGEJO_URL=https://git.example.com
- WOODPECKER_FORGEJO_CLIENT=${FORGEJO_CLIENT_ID}
- WOODPECKER_FORGEJO_SECRET=${FORGEJO_CLIENT_SECRET}
- WOODPECKER_DATABASE_DRIVER=postgres
- WOODPECKER_DATABASE_DATASOURCE=postgres://woodpecker:${POSTGRES_PASSWORD}@postgres:5432/woodpecker?sslmode=disable
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
ports:
- "127.0.0.1:8000:8000"
- "127.0.0.1:9000:9000"
volumes:
- ./woodpecker-data:/var/lib/woodpecker
depends_on:
postgres:
condition: service_healthy
networks:
- woodpecker-net
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: unless-stopped
env_file: .env
environment:
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
- WOODPECKER_BACKEND=docker
- WOODPECKER_MAX_WORKFLOWS=4
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /tmp/woodpecker-agent:/tmp
depends_on:
- woodpecker-server
networks:
- woodpecker-net
postgres:
image: postgres:16-alpine
container_name: woodpecker-postgres
restart: unless-stopped
environment:
- POSTGRES_DB=woodpecker
- POSTGRES_USER=woodpecker
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U woodpecker -d woodpecker"]
interval: 10s
timeout: 5s
retries: 5
networks:
- woodpecker-net
networks:
woodpecker-net:
driver: bridgeIf the copy button does not work in your browser, manually copy the command block above.
4) Configure Caddy
Add a site block to your Caddyfile. Caddy will automatically provision and renew a TLS certificate from Let's Encrypt. Replace ci.example.com with your actual domain.
ci.example.com {
reverse_proxy 127.0.0.1:8000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
}
}If the copy button does not work in your browser, manually copy the command block above.
sudo systemctl reload caddyIf the copy button does not work in your browser, manually copy the command block above.
5) Create the OAuth application in Forgejo
Log into your Forgejo instance as an admin. Navigate to Site Administration → Applications → OAuth2 Applications and create a new application with the redirect URI set to https://ci.example.com/authorize. Copy the client ID and client secret into your .env file.
6) Start the stack
cd /opt/woodpecker
docker compose up -d
docker compose psIf the copy button does not work in your browser, manually copy the command block above.
Configuration and secrets handling
Store all credentials in the .env file and never commit it to version control. For the agent secret, generate a strong value with:
openssl rand -hex 32If the copy button does not work in your browser, manually copy the command block above.
Woodpecker also supports per-repository and per-organization pipeline secrets that are injected at runtime — never hard-code sensitive values in your .woodpecker.yml pipeline files. Manage pipeline-level secrets from the repository settings page in the Woodpecker UI or via the API.
For the PostgreSQL password, use a random 32-character alphanumeric string. Avoid special characters that require shell escaping in the DSN. Back up the contents of /opt/woodpecker/postgres-data regularly using pg_dump or a volume snapshot tool such as Kopia.
Verification
After startup, run the following checks to confirm the stack is healthy before routing traffic or onboarding users.
# Check all containers are running
docker compose ps
# Verify server responds on localhost
curl -sf http://127.0.0.1:8000/healthz && echo "server healthy"
# Check Caddy is proxying correctly
curl -sf https://ci.example.com/healthz && echo "caddy routing OK"
# Confirm the agent is connected
docker compose logs woodpecker-agent | grep "connect to server"
# Check PostgreSQL is accepting queries
docker compose exec postgres psql -U woodpecker -d woodpecker -c "SELECT COUNT(*) FROM users;"If the copy button does not work in your browser, manually copy the command block above.
Common issues and fixes
Agent cannot connect to server: Verify that WOODPECKER_AGENT_SECRET matches exactly in both the server and agent environment. Check network connectivity with docker compose exec woodpecker-agent nc -zv woodpecker-server 9000. Ensure the woodpecker-net network was created and both containers are attached.
OAuth redirect loop: The WOODPECKER_HOST value must exactly match the registered OAuth redirect URI in Forgejo, including the scheme and no trailing slash. If behind a reverse proxy, also verify Caddy is passing the correct X-Forwarded-Proto: https header.
Pipeline containers fail to start: The agent container must have access to /var/run/docker.sock. Confirm with docker compose exec woodpecker-agent docker ps. If the agent user lacks permissions, add it to the docker group on the host and restart the agent container.
Database connection errors: Check that the POSTGRES_PASSWORD in the DSN matches the value used when PostgreSQL was initialised. If you changed the password after first startup, the existing data directory retains the original password. Use docker compose exec postgres psql -U woodpecker to verify connectivity directly.
TLS certificate not provisioning: Confirm port 80 and 443 are open and DNS is resolving to the correct IP. Review Caddy logs with sudo journalctl -u caddy -n 100. Caddy requires port 80 to complete the ACME HTTP-01 challenge even when serving HTTPS.
FAQ
Can I use Gitea instead of Forgejo as the Git backend?
Yes. Woodpecker supports both. Replace the WOODPECKER_FORGEJO and WOODPECKER_FORGEJO_URL environment variables with WOODPECKER_GITEA=true and WOODPECKER_GITEA_URL. The OAuth application registration steps are identical.
How do I scale agents horizontally?
Add additional woodpecker-agent service blocks to your Compose file with distinct container names, or run the agent image on separate Docker hosts. All agents connect to the same server gRPC address and share the same agent secret. Increase WOODPECKER_MAX_WORKFLOWS on each agent to control concurrency per host.
What pipeline syntax does Woodpecker use?
Woodpecker uses a YAML file at .woodpecker.yml (or .woodpecker/*.yml for multi-pipeline repos). Each pipeline defines steps as Docker image + command pairs, with optional when conditions, environment injection, and secret references. The syntax is intentionally simple and closely resembles the original Drone CI format.
How do I back up and restore Woodpecker?
Stop the stack, then dump PostgreSQL with pg_dump -U woodpecker woodpecker > backup.sql and archive /opt/woodpecker/woodpecker-data. To restore, recreate the database, restore the SQL dump, and restart the stack. Pipeline artifacts stored outside the database (e.g., in volume mounts) should be backed up separately.
Can I restrict which users can log in?
Yes. Set WOODPECKER_OPEN=false (already in the template) so only users explicitly added by an admin can access the instance. You can also allowlist specific users with WOODPECKER_ADMIN=username1,username2 and block self-registration entirely for tighter access control.
How do I upgrade Woodpecker to a new version?
Woodpecker follows semantic versioning and publishes changelogs for every release. To upgrade, update the image tags in your Compose file to the desired version, run docker compose pull, then docker compose up -d. Always review the release notes for migration steps before upgrading across major versions, and take a database backup beforehand.
Internal links
- Production Guide: Deploy Forgejo with Docker Compose + Caddy + PostgreSQL + SSH on Ubuntu
- Production Guide: Deploy Gitea with Docker Compose + Traefik + PostgreSQL on Ubuntu
- Production Guide: Deploy Drone CI with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.