Self-Hosted CI/CD with Woodpecker CI on a VPS: Pipelines, Docker Builds, and Auto-Deploy
Woodpecker CI is a lightweight, open-source CI/CD system forked from Drone CI. It runs pipeline steps in Docker containers, integrates with Gitea, GitHub, GitLab, and Forgejo, and requires just 50–100 MB RAM for the server. This guide installs Woodpecker CI with Docker Compose, connects it to a Gitea repository, and builds pipelines for testing, Docker image builds, and automatic server deployment.
Why Woodpecker Over GitHub Actions or GitLab CI
- Self-hosted: Pipelines run on your own VPS — no per-minute billing, no data leaving your infrastructure
- Lightweight: 50–100 MB RAM for the server, compared to Jenkins’ 512 MB+
- GitHub Actions-like syntax: Familiar YAML pipeline format with steps and services
- Works with Gitea: Perfect companion for a self-hosted Gitea instance
- Docker-native: Each step runs in a fresh container — no environment contamination between builds
Architecture
Developer pushes code
→ Gitea webhook fires
→ Woodpecker Server receives webhook
→ Woodpecker Agent pulls pipeline config (.woodpecker.yml)
→ Agent runs steps in Docker containers
→ Results reported back to Gitea (commit status)
Step 1: Prerequisites
# Docker and Docker Compose must be installed
docker --version
docker compose version
# Woodpecker needs an OAuth app in your Git provider
# For Gitea: Admin → Settings → Applications → OAuth2 Apps → Add
Step 2: Create Gitea OAuth Application
- In Gitea: User Settings → Applications → OAuth2 Applications → Create OAuth2 Application
- Application Name:
Woodpecker CI - Redirect URI:
https://ci.yourdomain.com/authorize - Copy the Client ID and Client Secret
Step 3: Docker Compose Setup
mkdir -p /opt/woodpecker && cd /opt/woodpecker
nano docker-compose.yml
version: '3.8'
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:latest
container_name: woodpecker-server
restart: always
ports:
- "127.0.0.1:8000:8000"
- "127.0.0.1:9000:9000"
environment:
WOODPECKER_OPEN: "false" # Disable open registration
WOODPECKER_HOST: https://ci.yourdomain.com
WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: https://git.yourdomain.com
WOODPECKER_GITEA_CLIENT: ${GITEA_CLIENT_ID}
WOODPECKER_GITEA_SECRET: ${GITEA_CLIENT_SECRET}
WOODPECKER_AGENT_SECRET: ${AGENT_SECRET}
WOODPECKER_ADMIN: your_gitea_username
WOODPECKER_DATABASE_DRIVER: sqlite3
WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite
volumes:
- woodpecker_data:/var/lib/woodpecker/
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
container_name: woodpecker-agent
restart: always
command: agent
environment:
WOODPECKER_SERVER: woodpecker-server:9000
WOODPECKER_AGENT_SECRET: ${AGENT_SECRET}
WOODPECKER_MAX_WORKFLOWS: 4
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- woodpecker-server
volumes:
woodpecker_data:
nano .env
GITEA_CLIENT_ID=your_gitea_oauth_client_id
GITEA_CLIENT_SECRET=your_gitea_oauth_client_secret
# Generate: openssl rand -hex 32
AGENT_SECRET=your_agent_secret_min_32_chars
chmod 600 .env
docker compose up -d
docker compose logs -f woodpecker-server
Step 4: Nginx Reverse Proxy
sudo nano /etc/nginx/sites-available/woodpecker
server {
listen 80;
server_name ci.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ci.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/ci.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ci.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
sudo ln -s /etc/nginx/sites-available/woodpecker /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d ci.yourdomain.com
Step 5: Activate a Repository
- Visit
https://ci.yourdomain.comand log in with Gitea OAuth - Click your avatar → Repos → find your repository → toggle to activate
- Woodpecker adds a webhook to your Gitea repository automatically
Step 6: Write Pipelines
Node.js Test + Build Pipeline
# .woodpecker.yml in repository root
steps:
- name: install
image: node:20-alpine
commands:
- npm ci
- name: test
image: node:20-alpine
commands:
- npm test
depends_on: [install]
- name: build
image: node:20-alpine
commands:
- npm run build
when:
branch: main
depends_on: [test]
Docker Image Build and Push
steps:
- name: test
image: python:3.12-alpine
commands:
- pip install -r requirements.txt
- pytest
- name: build-and-push
image: woodpeckerci/plugin-docker-buildx
settings:
repo: ghcr.io/yourorg/myapp
tags: latest,${CI_COMMIT_SHA:0:8}
username:
from_secret: registry_username
password:
from_secret: registry_password
when:
branch: main
depends_on: [test]
Automatic Deployment on Push
steps:
- name: test
image: node:20-alpine
commands:
- npm ci && npm test
- name: build
image: node:20-alpine
commands:
- npm run build
depends_on: [test]
- name: deploy
image: alpine
environment:
DEPLOY_HOST:
from_secret: deploy_host
DEPLOY_KEY:
from_secret: deploy_ssh_key
commands:
- apk add --no-cache openssh-client rsync
- mkdir -p ~/.ssh && echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
- echo "StrictHostKeyChecking no" >> ~/.ssh/config
- rsync -avz --delete dist/ deploy@$DEPLOY_HOST:/var/www/myapp/
- ssh deploy@$DEPLOY_HOST "sudo systemctl reload nginx"
when:
branch: main
depends_on: [build]
Step 7: Add Secrets
- In Woodpecker dashboard → your repository → Settings → Secrets
- Add:
deploy_host(your production VPS IP),deploy_ssh_key(private SSH key),registry_username,registry_password - Secrets are injected as environment variables and never logged
Pipeline Services (Database for Tests)
services:
- name: postgres
image: postgres:16-alpine
environment:
POSTGRES_DB: test_db
POSTGRES_USER: test
POSTGRES_PASSWORD: testpass
steps:
- name: test
image: python:3.12
environment:
DATABASE_URL: postgresql://test:testpass@postgres:5432/test_db
commands:
- pip install -r requirements.txt
- pytest --cov=app tests/
Getting Started
Woodpecker CI pairs naturally with a self-hosted Gitea instance on a Ubuntu VPS at VPS.DO. The two services together — Gitea (source code) and Woodpecker (CI/CD) — provide a complete self-hosted DevOps platform at the cost of one VPS plan, with no per-minute pipeline billing. A 2 vCPU / 4 GB RAM VPS comfortably runs both services and handles 4 parallel pipeline workers.
Conclusion
Woodpecker CI delivers GitHub Actions-style CI/CD on your own VPS: Docker-based pipeline steps, multi-stage builds, secret management, and webhook-triggered automation. Combined with Gitea, it creates a complete self-hosted development platform — source code, CI/CD, and container registry — without subscription costs or data leaving your infrastructure.