Automation8 min read

Replace GitHub Actions with Woodpecker CI on a VPS

In December 2025, GitHub announced per-minute billing for self-hosted runners on private repositories (community discussion #182089, 72 upvotes). Faced with a massive community backlash, the pricing was suspended within 48 hours — and as of September 1, 2026, self-hosted runners remain free. The threat is real, however: nothing prevents GitHub from re-activating it. Migrating to Woodpecker CI means protecting yourself against the next announcement — and gaining full sovereignty over your CI/CD in the process. Woodpecker CI is an open source alternative that runs in under 50 MB of RAM, connects to Gitea or Forgejo via OAuth2, and costs nothing beyond your VPS.

GitHub and runner pricing: what happened in 2025-2026

Until 2025, private repositories included a monthly quota of free minutes. On December 16, 2025, GitHub announced per-minute pricing for runner usage — including self-hosted runners running on your own machines. GitHub Discussion #182089 (opened that same month, 72 upvotes) captured the community's concern: teams would be paying GitHub to orchestrate a job running on their own infrastructure, at $0.002 per minute.

Faced with a massive community backlash, GitHub suspended the pricing within 48 hours. As of September 1, 2026, self-hosted runners on private repositories remain free — the measure was never enforced. But the announcement exposed a structural dependency: GitHub can change its terms at any time. Moving the entire CI/CD chain to a self-hosted forge is how you remove that uncertainty. Woodpecker CI is built precisely for this: a lightweight, open source (Apache 2.0) pipeline engine that installs alongside Gitea or Forgejo and has no minute counter.

GitHub Actions runners vs self-hosted Woodpecker CI

CriterionGitHub Actions (cloud runners)Woodpecker CI on VPS
Cost$0.008 / min (Linux) + $0.002 / min orchestration on private reposIncluded in the fixed VPS cost (from `{{vps.start.price}}`)
CI engine RAMGitHub-hosted runners: shared resources, not under your controlWoodpecker server + agent: under 50 MB RAM combined
SovereigntyCode and secrets transit through GitHub/Microsoft infrastructureEverything stays on your VPS — no external transit
Workflow syntax`.github/workflows/*.yml` — widely adopted standard`.woodpecker.yml` — clean, shorter syntax, not compatible with GitHub Actions
Forge requirementNone (GitHub hosts the repository)Self-hosted Gitea or Forgejo (or GitHub as remote forge)
LicenceProprietaryApache 2.0

Prerequisites before installation

To deploy Woodpecker CI comfortably, check the following requirements.

VPS: 1 vCPU and 1 GB of RAM are sufficient for the Woodpecker server, one agent and a lightweight Gitea or Forgejo forge. Plan for 2 GB if you run several builds in parallel or if your jobs build Docker images.

Software: Docker and Docker Compose must be installed. If you are starting from a bare VPS, the guide Getting started with Docker on a VPS covers this step.

Git forge: Woodpecker CI relies on OAuth2 for authentication. You need a Gitea or Forgejo instance already running and accessible via HTTPS (for example at git.your-domain.com). The guides Hosting Gitea and Hosting Forgejo detail this setup.

Domain: prepare a dedicated subdomain for Woodpecker — for example ci.your-domain.com — pointing to your VPS IP, with port 443 open. This subdomain serves as WOODPECKER_HOST and as the OAuth2 redirect URL.

Install Woodpecker CI with Docker Compose

01

Create an OAuth2 application in Gitea

In Gitea, go to Settings → Applications → Manage OAuth2 Applications. Give the application a name (for example woodpecker) and enter the redirect URL: https://ci.your-domain.com/authorize. Note the generated Client ID and Client Secret — you will need them in the next step.

If you use Forgejo, the procedure is identical: Settings → Applications → OAuth2.

02

Create the docker-compose.yml file

Create the /opt/woodpecker directory and place a docker-compose.yml file with the two services — server and agent:

services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    restart: always
    ports:
      - "8000:8000"
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=false
      - WOODPECKER_HOST=https://ci.your-domain.com
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=https://git.your-domain.com
      - WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
      - WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    restart: always
    command: agent
    depends_on:
      - woodpecker-server
    volumes:
      - woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}

volumes:
  woodpecker-server-data:
  woodpecker-agent-config:

The server listens internally on port 8000 (web interface) and 9000 (gRPC, agent communication). Do not expose these ports directly: a reverse proxy handles TLS.

03

Create the .env file

In /opt/woodpecker, create a .env file containing the three sensitive variables:

WOODPECKER_GITEA_CLIENT=<oauth2-client-id>
WOODPECKER_GITEA_SECRET=<oauth2-client-secret>
WOODPECKER_AGENT_SECRET=<long-random-string>

Generate WOODPECKER_AGENT_SECRET with openssl rand -hex 32. This string authenticates the agent with the server — do not share it.

04

Configure the nginx reverse proxy

Place Woodpecker behind nginx with a Let's Encrypt certificate. Example server block for ci.your-domain.com:

server {
    listen 443 ssl;
    server_name ci.your-domain.com;
    ssl_certificate /etc/letsencrypt/live/ci.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ci.your-domain.com/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }
}

Obtain the certificate with certbot certonly --nginx -d ci.your-domain.com then reload nginx with systemctl reload nginx.

05

Start the stack

From /opt/woodpecker, run:

docker compose up -d

Watch the server logs until you see the ready line:

docker compose logs -f woodpecker-server

Open https://ci.your-domain.com in your browser. The login page shows a Sign in via Gitea button. Click it: Gitea asks for OAuth2 authorisation, then redirects you to the Woodpecker dashboard.

06

Activate a repository and trigger the first build

In the Woodpecker dashboard, click + Add repository. Your list of Gitea repositories appears. Select a project and activate it. Woodpecker automatically registers a webhook in Gitea to trigger builds on each push.

Push a first commit to this repository: if the .woodpecker.yml file is present at the root, the pipeline triggers immediately in the interface.

Migrating a GitHub Actions workflow to .woodpecker.yml

Woodpecker CI has its own YAML syntax — it is not compatible with GitHub Actions. The good news: it is shorter. Here is the migration of a classic Node.js workflow.

Before — .github/workflows/ci.yml:

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test

After — .woodpecker.yml:

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

Key differences: Woodpecker uses a Docker image directly as the runtime (no setup-node needed), and each step is a container service. The checkout action is implicit — Woodpecker automatically clones the repository into the shared workspace between steps.

Pipeline secrets and the WOODPECKER_SECRET variable

Never put tokens, SSH keys or passwords in your versioned .woodpecker.yml. Woodpecker manages secrets at the repository or organisation level: in the dashboard, open the repository → Settings → Secrets → Add secret. Each secret is injected as an environment variable at build time.

To use a secret in the pipeline:

steps:
  - name: deploy
    image: alpine
    environment:
      SSH_DEPLOY_KEY:
        from_secret: ssh_deploy_key
    commands:
      - echo "$SSH_DEPLOY_KEY" > /tmp/id_rsa
      - chmod 600 /tmp/id_rsa
      - ssh -i /tmp/id_rsa [email protected] ./deploy.sh

The from_secret syntax extracts the value from the Woodpecker vault without writing it to logs or to the YAML file.

Common troubleshooting

The agent does not appear in the dashboard. Check that WOODPECKER_AGENT_SECRET is identical in both services of docker-compose.yml. If you modified the .env after the first start, restart with docker compose down && docker compose up -d.

OAuth2 login fails. The redirect URL registered in Gitea must match exactly https://ci.your-domain.com/authorize. A trailing slash, a typo or HTTP instead of HTTPS are enough to block the OAuth2 flow. Also check that WOODPECKER_GITEA_URL in docker-compose.yml points to the public URL of your Gitea — the Woodpecker container must be able to resolve it from inside the Docker network.

The webhook does not trigger. Open the repository in Gitea → Settings → Webhooks and check that the Woodpecker webhook is present and that recent deliveries have no 4xx error. If the webhook URL points to http:// instead of https://, Gitea may refuse to call it. Reconfigure WOODPECKER_HOST with the full HTTPS URL and reactivate the repository in Woodpecker to force the webhook update.

Builds fail due to out of memory. The Woodpecker server and agent use under 50 MB at idle — but each build step launches an additional Docker container. On a 1 GB VPS, limit concurrent builds by adding WOODPECKER_MAX_PROCS=2 to the woodpecker-agent service environment.

Going further

Once the base pipeline is working, several optimisations are worth exploring.

Multi-runner. Add a second woodpecker-agent service to your docker-compose.yml (same image, same WOODPECKER_AGENT_SECRET) to parallelise builds. Each agent is independent and can run on a separate VPS if your workload requires it.

Docker cache between builds. Mount a Docker volume in the agent and enable Docker layer caching to avoid re-downloading images on each build. Woodpecker supports native pipeline caching via the woodpecker-ci/cache plugin — add it as a dedicated step in your .woodpecker.yml.

Notifications. Woodpecker can notify a messaging channel on each build. Community plugins cover Matrix, Slack, Telegram and Mattermost — look for woodpecker-ci/notify-matrix or appleboy/drone-telegram in the official documentation.

Conditional pipelines. Woodpecker syntax lets you restrict a step to a specific branch or event:

steps:
  - name: deploy
    image: alpine
    when:
      branch: main
      event: push
    commands:
      - ./deploy.sh

To go deeper into pipeline setup, the guide Woodpecker CI and Forgejo: CI/CD pipeline on a VPS details advanced steps including Forgejo integration and multi-stage pipelines. The guide Docker Compose production checklist covers best practices for securing your Docker stacks in production.

A ServOrbit VPS for your CI/CD pipeline

The `{{vps.start.name}}` plan at `{{vps.start.price}}` — 1 vCPU, 1 GB RAM, fixed IP — is enough to run Woodpecker CI, one agent and a Gitea forge in parallel, with no minute counter.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab