Chapter 5 Sidequest
Sidequest: Self-Hosted CI with Woodpecker
Run your own CI/CD server on your homelab.
This sidequest requires:
- A server you control (the one running your Minecraft server works!)
- Comfort with Docker and Docker Compose
- About an hour of setup time
If you're just learning CI/CD, start with GitHub Actions in the main chapter. Come back here when you want more control.
GitHub Actions is great, but it runs on GitHub's computers. What if you want CI/CD that runs on your hardware? Maybe for privacy, maybe for cost (free tiers have limits), maybe just because you can?
That's where self-hosted CI comes in. We'll use Woodpecker—it's lightweight, Docker-native, and designed for homelabs.
Why Self-Hosted?
Why would I run my own CI server when GitHub does it for free?
"A few reasons," I said.
"First: privacy. With GitHub Actions, your code and builds run on Microsoft's servers. That's fine for most things, but some people want everything to stay local."
"Second: limits. GitHub's free tier gives you 2,000 minutes per month. For a small project, plenty. For a homelab with lots of repos, you'll hit it."
"Third: learning. Running your own infrastructure teaches you how these systems actually work, not just how to use them."
Setting Up Woodpecker
Woodpecker has two parts:
- Server: The brain that manages workflows
- Agent: The worker that runs jobs
For a homelab, you'll run both on the same machine.
services: woodpecker-server: image: woodpeckerci/woodpecker-server:latest container_name: woodpecker-server ports: - "8000:8000" volumes: - woodpecker-data:/var/lib/woodpecker environment: - WOODPECKER_OPEN=true - WOODPECKER_HOST=http://localhost:8000 - WOODPECKER_GITHUB=true - WOODPECKER_GITHUB_CLIENT=${GITHUB_CLIENT_ID} - WOODPECKER_GITHUB_SECRET=${GITHUB_CLIENT_SECRET} - WOODPECKER_AGENT_SECRET=your-agent-secret-here restart: unless-stopped
woodpecker-agent: image: woodpeckerci/woodpecker-agent:latest container_name: woodpecker-agent depends_on: - woodpecker-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - WOODPECKER_SERVER=woodpecker-server:9000 - WOODPECKER_AGENT_SECRET=your-agent-secret-here restart: unless-stopped
volumes: woodpecker-data:
Woodpecker needs to connect to GitHub to watch your repos. You'll need to create a GitHub OAuth app:
- Go to GitHub → Settings → Developer Settings → OAuth Apps
- Create a new app with callback URL:
http://your-server:8000/authorize - Copy the Client ID and Client Secret
- Put them in a
.envfile (don't commit this!)
GITHUB_CLIENT_ID=your_client_id_here GITHUB_CLIENT_SECRET=your_client_secret_here
$ docker compose -f docker-compose.woodpecker.yml up -d
[+] Running 3/3
✔ Network woodpecker_default Created
✔ Container woodpecker-server Started
✔ Container woodpecker-agent Started
Open http://your-server:8000 in your browser. Log in with GitHub. You should see your repositories!
Woodpecker Pipelines
Woodpecker pipelines look similar to GitHub Actions, but with some differences. The config goes in .woodpecker.yml in your repo.
steps:
- name: validate
image: docker
commands:
- docker compose config --quiet
- echo "Config is valid!"
- name: check-eula
image: alpine
commands:
- grep -q 'EULA.*TRUE' docker-compose.yml
- echo "EULA accepted!"
It looks similar but... where's the runs-on part?
"In Woodpecker, each step runs in a Docker container you specify. Instead of 'runs-on: ubuntu-latest', you say 'image: alpine' or 'image: docker'. It's more explicit about what environment you're using."
Key Differences from GitHub Actions
| Feature | GitHub Actions | Woodpecker |
|---|---|---|
| Config file | .github/workflows/*.yml | .woodpecker.yml |
| Environment | runs-on: ubuntu-latest | image: alpine |
| Pre-built actions | uses: actions/checkout@v4 | Not as common |
| Cloning | Needs checkout action | Auto-clones by default |
| Cost | Free tier with limits | Free (you pay for server) |
"The biggest difference is that Woodpecker auto-clones your repo at the start of each pipeline. You don't need a checkout step."
A Complete Example
steps:
- name: validate-compose
image: docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock commands:
- docker compose config --quiet
- echo "Docker Compose config is valid"
- name: check-requirements
image: alpine
commands:
- grep -q 'EULA.*TRUE' docker-compose.yml && echo "EULA OK"
- grep -q '19132:19132/udp' docker-compose.yml && echo "Port OK"
- echo "All checks passed!"
- name: notify
image: alpine
when:
status: failure
commands:
- echo "Something failed! Check the logs."
When to Use Self-Hosted
"So should I use Woodpecker instead of GitHub Actions?" Max asked.
"Not instead—it depends on what you need."
Use GitHub Actions when:
- You're just starting out
- Your project is on GitHub anyway
- You don't want to manage infrastructure
- You're under the free tier limits
Use Woodpecker when:
- You want full control
- You need unlimited build minutes
- You want to keep everything on your network
- You're learning how CI systems actually work
- You have a homelab and want to use it!
"For your Minecraft server project, GitHub Actions is fine. But if you ever run a homelab with a dozen repos, self-hosted CI starts making sense."
You've set up your own CI server:
- Woodpecker for lightweight self-hosted CI
- OAuth connection to GitHub
- Pipeline files in
.woodpecker.yml
# Basic Woodpecker pipeline
steps:
- name: my-step
image: alpine
commands:
- echo "Hello from your own CI!"