Back to blog
Blog

Self-Host Woodpecker CI With Gitea on a VPS

Self-host Woodpecker CI with Gitea on a VPS: Docker Compose, HTTPS reverse proxy, runners, updates, and backups.

By Anurag Singh
Updated on Nov 05, 2026
Category: Blog
Share article
Self-Host Woodpecker CI With Gitea on a VPS

CI doesn’t have to live inside a giant platform. If you already run your own Git server (or plan to), Gitea plus Woodpecker gives you a pipeline you can trace end-to-end.

This post shows how to self-host Woodpecker CI next to Gitea on a VPS. The setup is production-minded, with HTTPS, a runner, upgrades, and backups.

Woodpecker stays intentionally small. You get “pipeline-as-code” and sensible defaults without buying into an all-in-one suite.

For teams with a few services, private repos, and a fixed infrastructure budget, that trade-off usually fits.

Why Woodpecker + Gitea works well for small teams

Gitea covers the daily workflow: Git hosting, issues, pull requests, and users. Woodpecker adds build/test/deploy pipelines triggered by pushes and pull requests.

You control where jobs run, which images are allowed, and how secrets are managed.

  • Fewer moving parts than GitLab: you can run it on a modest VPS without constant resource drama.
  • No per-minute billing: capacity is whatever your runner host can sustain.
  • Plays nicely with “boring” deploys: rsync to a VPS, build Docker images, run migrations, restart systemd services.

If you use GitHub Actions mostly because it’s bundled with repo hosting, this stack is a clean way out. It still feels current.

self-host Woodpecker CI vs GitHub Actions (and other hosted CI)

Hosted CI is great until the edges show. Most teams hit the same two pressure points: control and cost.

  • Network access: self-hosted runners can reach private databases, internal HTTP services, staging “just a VPS” environments, and SSH-only servers without awkward tunnels.
  • Compliance: logs and artifacts stay on your own infrastructure, which matters for regulated clients.
  • Predictable spend: your VPS bill doesn’t jump because a test suite took longer than usual.

The trade-offs are real. Hosted CI often wins on prebuilt integrations and easy macOS/Windows runners.

If your work is mostly Linux builds and deployments, Woodpecker usually covers what you need.

What you’ll run (components, ports, and data paths)

A practical deployment has three parts:

  • Gitea server (web UI + Git over SSH/HTTP)
  • Woodpecker server (web UI + API + OAuth integration with Gitea)
  • Woodpecker runner (executes your pipelines; typically the Docker runner)

Keep persistent data out of containers. A simple pattern is:

  • /srv/gitea for Gitea data
  • /srv/woodpecker for Woodpecker state
  • /srv/backups for backup archives

Expose HTTPS for Gitea and Woodpecker to the internet. Keep app ports private with Docker networking.

Keep SSH (port 22) on the host for Git SSH and admin access.

VPS sizing: pick boring resources that won’t hurt later

CI load comes in spikes. If your team pushes a few times a day, start small.

Plan to scale runners later.

  • Small team (1–5 devs): 2 vCPU, 4 GB RAM, 60+ GB NVMe is a comfortable baseline.
  • Growing team (6–15 devs): 4 vCPU, 8 GB RAM, 100+ GB NVMe keeps queue time reasonable.
  • Heavy Docker builds: prioritize CPU before RAM; builds are usually CPU and IO bound.

If you don’t want to babysit OS updates, Docker upgrades, and monitoring, managed VPS hosting is a simple way to keep a self-hosted CI stack steady.

If you prefer full control, start with a HostMyCode VPS and run it like any other production service.

Deployment overview with Docker Compose (Gitea + Woodpecker)

This isn’t meant to be a blind copy/paste recipe. You should still understand what you’re running.

The main idea is simple. Keep services on a private Docker network, and reverse-proxy only the web UIs.

Create a working directory:

sudo mkdir -p /srv/ci-stack
cd /srv/ci-stack

A single docker-compose.yml can run the stack. Use images and tags you’ve validated in your environment.

Don’t blindly follow “latest” in production.

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
    volumes:
      - /srv/gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - internal

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker-server
    environment:
      # Woodpecker base config
      - WOODPECKER_HOST=https://ci.example.com
      - WOODPECKER_OPEN=false
      - WOODPECKER_ADMIN=yourusername

      # OAuth with Gitea
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=https://git.example.com
      - WOODPECKER_GITEA_CLIENT=REPLACE_WITH_OAUTH_CLIENT_ID
      - WOODPECKER_GITEA_SECRET=REPLACE_WITH_OAUTH_CLIENT_SECRET

      # A shared secret used by runners
      - WOODPECKER_AGENT_SECRET=REPLACE_WITH_LONG_RANDOM

    volumes:
      - /srv/woodpecker:/var/lib/woodpecker
    depends_on:
      - gitea
    networks:
      - internal

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker-agent
    command: agent
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=REPLACE_WITH_LONG_RANDOM
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - woodpecker-server
    networks:
      - internal

networks:
  internal:
    driver: bridge

Two practical notes:

  • Docker socket access gives the agent significant power on the host. Keep this VPS dedicated to CI, or isolate runners on separate machines.
  • OAuth credentials come from Gitea. Create an OAuth2 app in the Gitea admin UI, then set the callback URL to https://ci.example.com/authorize.

Reverse proxy + HTTPS: Caddy or Nginx, keep it predictable

Don’t publish Woodpecker or Gitea container ports to the public internet. Put a reverse proxy in front.

Let it handle TLS, HTTP/2, and basic headers.

Caddy is a good fit because it issues and renews Let’s Encrypt certificates automatically.

If you already use Caddy elsewhere, keep the same approach as our guide on Caddy reverse proxy for Docker. For certificate flow and common pitfalls, see Let’s Encrypt with Caddy.

A minimal Caddyfile (stored at /etc/caddy/Caddyfile) looks like this:

git.example.com {
  reverse_proxy 127.0.0.1:3000
}

ci.example.com {
  reverse_proxy 127.0.0.1:8000
}

To match that file, publish local ports from the containers to localhost only (not to the internet).

Many operators do this by adding ports: bindings like 127.0.0.1:3000:3000 for Gitea and 127.0.0.1:8000:8000 for Woodpecker.

Another option is to run Caddy in Docker on the same network. Either is fine. Pick one pattern and stick to it.

If you prefer Nginx, watch request sizes. Repo pushes and CI logs can exceed defaults.

If you hit that limit, the fix is usually quick—see Nginx 413 troubleshooting.

Runner strategy: one local agent now, more later

Most teams start with a single Docker runner on the same VPS as Woodpecker. That’s fine for unit tests, linting, and small-to-medium Docker builds.

When jobs start stacking up, split the workload:

  • Keep Woodpecker server + Gitea on a “control” VPS (stable, backed up, low churn).
  • Add one or more runner VPS nodes that only run the Woodpecker agent.

This also limits damage. A pipeline that floods disk or produces enormous logs shouldn’t be able to knock out your Git UI.

One habit that pays off: treat runner nodes as disposable. Build them from a repeatable setup.

Rotate secrets. Re-provision when they drift.

If you’re running Ubuntu or Debian, enable automated security updates (and verify reboots) using a process like the one in automated security updates on Ubuntu VPS.

What a Woodpecker pipeline looks like (realistic example)

Woodpecker reads a YAML file in your repo (commonly .woodpecker.yml). The format is intentionally plain.

Steps run in containers. Pass secrets for deploy keys or registry tokens as needed.

steps:
  test:
    image: node:20
    commands:
      - npm ci
      - npm test

  build_image:
    image: docker:27
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    commands:
      - docker build -t registry.example.com/myapp:${CI_COMMIT_SHA} .
      - docker push registry.example.com/myapp:${CI_COMMIT_SHA}
    when:
      event:
        - push

Start with boring pipelines. First, get reliable build-and-test runs.

Then add deployments, matrix builds, or caching.

Backups: what to capture, how often, and how to test restores

Backups for this stack come down to state: Git repositories, database files (if you use an external DB), configuration, and Woodpecker metadata.

Your checklist depends on your Gitea setup (SQLite vs PostgreSQL/MySQL). The rule stays the same: back up what you can’t recreate.

At minimum, capture:

  • /srv/gitea (repositories, attachments, configuration, and DB if using SQLite)
  • /srv/woodpecker (Woodpecker server data)
  • Reverse proxy config (for example /etc/caddy/Caddyfile and /etc/caddy)
  • Your Compose file and any .env secrets stored on disk

A simple, workable approach is a nightly tarball pushed offsite (object storage, another VPS, or your backup server).

Example sketch:

sudo mkdir -p /srv/backups
sudo tar -C /srv -czf /srv/backups/ci-stack-$(date +%F).tar.gz gitea woodpecker
sudo tar -C /etc -czf /srv/backups/caddy-$(date +%F).tar.gz caddy

If you want encrypted, deduplicated backups with retention policies, Borg remains a common choice for VPS operators.

Whatever you pick, do the part most teams skip: restore onto a scratch VPS once. Confirm Gitea and Woodpecker actually come up.

Upgrade workflow: reduce surprises, keep downtime short

Upgrades are where self-hosting either feels routine or turns into a fire drill. Keep the process repeatable:

  1. Read the release notes for Gitea and Woodpecker before pulling new images.
  2. Take a fresh backup (and confirm it exists off the box if possible).
  3. Pull images and restart during a quiet window.
cd /srv/ci-stack
docker compose pull
docker compose up -d
docker image prune -f

If you want a smaller blast radius, put Woodpecker and Gitea on separate VPS instances.

That’s also where a dedicated server can make sense. You isolate build workloads, get consistent CPU performance, and avoid noisy neighbors during large Docker builds.

Operational gotchas (things that break first)

  • Clock drift: OAuth and TLS are unforgiving when system time is off. Install chrony and confirm it’s synced.
  • Disk growth: Docker layers and build caches eat storage quietly. Keep an eye on /var/lib/docker and prune aggressively.
  • Runner permissions: the Docker socket is root-equivalent access. Don’t run untrusted repos on the same runner used for production deploys.
  • Large pushes: tune reverse proxy limits and timeouts if you host big repos or LFS-like content.

Where HostMyCode fits (infrastructure that matches this stack)

Gitea + Woodpecker is small enough to fit on one VPS, but it’s still core infrastructure. Treat it that way.

Use solid storage, reliable backups, and a plan to add runner nodes when the queue grows.

If you manage your own servers, a HostMyCode VPS is a straightforward base for Docker Compose CI stacks. If you’d rather spend your time on software instead of patching, managed VPS hosting keeps the OS layer maintained while you keep control of the apps.

If you’re moving off hosted CI and want predictable performance, run Gitea + Woodpecker on a VPS you control. Start on a HostMyCode VPS, then move to managed VPS hosting once it becomes business-critical.

FAQ

Is Woodpecker CI open source, and what about licensing?

Woodpecker CI is an open-source project. Always verify the current license in the upstream repository before deploying in a company setting.

This matters even more if you bundle it into internal platforms.

Can I run Woodpecker with GitHub instead of Gitea?

Yes, Woodpecker supports multiple forges. This post focuses on Gitea because it keeps the whole workflow self-hosted and avoids vendor lock-in.

Should I put runners on the same VPS as the CI server?

For light workloads, it’s fine. If you build large Docker images or run CPU-heavy tests, move runners to separate VPS nodes.

That way a busy pipeline can’t degrade your Git and CI UI.

What’s the fastest way to back up Gitea repositories?

Back up the Gitea data directory (including repositories) and the database you selected.

Then test a restore on a scratch VPS so you know the backup is complete.

Do I need a dedicated server for this?

Not at first. A VPS handles most small teams. Dedicated hardware becomes attractive when builds run constantly, you need consistent CPU, or you’re isolating multiple runner pools for different trust levels.

Summary: a lightweight CI stack you can own

Woodpecker + Gitea hits a sweet spot in 2026. It’s easy enough to run on one VPS, but capable enough for real build-and-deploy workflows.

Keep the web UIs behind HTTPS. Treat runners as high-trust machines. Practice restores—not just backups.

If you want infrastructure sized for steady IO and Docker workloads, start with a HostMyCode VPS and add dedicated runner nodes as your queue grows.

Self-Host Woodpecker CI With Gitea on a VPS | HostMyCode