Why Self-Host?
There are three main reasons to run your own CI/CD stack: cost, control, and privacy. GitHub Actions free tier gives you 2,000 minutes per month - a medium-size project with multiple developers will exhaust this quickly. More importantly, if your code contains proprietary algorithms, unreleased product features, or security-sensitive logic, you may not want it running on shared cloud infrastructure.
Gitea is a lightweight, self-hosted Git service written in Go - think GitHub, but running on a $5/month VPS. Woodpecker CI was forked from Drone CI and integrates natively with Gitea. Together they form a complete DevOps platform with a resource footprint small enough to run alongside other services on a modest machine.
Docker Compose Setup
Both services are distributed as Docker images. Create a directory for your stack and write a docker-compose.yml:
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.example.com
- GITEA__server__ROOT_URL=https://git.example.com/
- GITEA__server__SSH_DOMAIN=git.example.com
ports:
- "3000:3000"
- "2222:22"
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: unless-stopped
environment:
- WOODPECKER_OPEN=false
- WOODPECKER_HOST=https://ci.example.com
- WOODPECKER_GITEA=true
- WOODPECKER_GITEA_URL=https://git.example.com
- WOODPECKER_GITEA_CLIENT=${GITEA_OAUTH_CLIENT}
- WOODPECKER_GITEA_SECRET=${GITEA_OAUTH_SECRET}
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
ports:
- "8000:8000"
volumes:
- ./woodpecker:/var/lib/woodpecker
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: unless-stopped
environment:
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- woodpecker-server
Gitea Setup and OAuth Application
Start Gitea first (docker compose up -d gitea) and complete the installation wizard at http://YOUR_IP:3000. Create your admin account. Then, to connect Woodpecker, create an OAuth2 application in Gitea:
- Go to Settings → Applications → OAuth2 Applications
- Application Name:
Woodpecker CI - Redirect URI:
https://ci.example.com/authorize - Save the Client ID and Client Secret - these go into your
.envfile
Create a .env file next to your docker-compose.yml:
GITEA_OAUTH_CLIENT=your_client_id_here
GITEA_OAUTH_SECRET=your_client_secret_here
WOODPECKER_AGENT_SECRET=a_long_random_secret_here
Generate the agent secret with: openssl rand -hex 32
Woodpecker Activation
Start the full stack with docker compose up -d. Navigate to https://ci.example.com and log in via Gitea OAuth. To activate CI on a repository, go to Repositories → Add Repository in the Woodpecker UI and enable it. Woodpecker automatically creates a webhook in Gitea.
Your First Pipeline
Woodpecker pipelines are defined in a .woodpecker.yml file at the root of your repository. Here's a practical example for a Node.js application:
# .woodpecker.yml
when:
- event: [push, pull_request]
steps:
- name: lint
image: node:20-alpine
commands:
- npm ci
- npm run lint
- name: test
image: node:20-alpine
commands:
- npm ci
- npm test
- name: build-image
image: plugins/docker
settings:
repo: registry.example.com/myapp
tags: latest,${CI_COMMIT_SHA:0:8}
username:
from_secret: registry_user
password:
from_secret: registry_password
when:
- branch: main
event: push
- name: deploy
image: alpine/ssh
commands:
- ssh [email protected] "docker compose pull && docker compose up -d --no-deps app"
when:
- branch: main
event: push
Managing Secrets
Secrets like registry credentials and SSH keys should never be hardcoded in your pipeline file. Add them in the Woodpecker UI under Repository → Settings → Secrets, then reference them with from_secret as shown above. Secrets are encrypted at rest and only injected into pipeline steps that explicitly request them.
ssh-keygen -t ed25519 -f woodpecker_deploy. Add the public key to your production server's ~/.ssh/authorized_keys with a command= restriction, and store the private key as a Woodpecker secret.