Deployment guide

Migrate CI/CD to Gitea or Forgejo: leave GitHub Actions

Deploy on a VPS Cloud →

Automation11 min read

Migrate CI/CD to Gitea or Forgejo: leave GitHub Actions

Since March 1, 2026, CI minutes on private GitHub repositories are billed per consumption. For teams that chain pipelines — tests, lint, Docker builds, deployments — the monthly bill can quickly exceed the infrastructure budget itself. Gitea and Forgejo let you take back control: same YAML syntax, same runner logic, zero cost per minute. This guide compares both options and walks through installation on a VPS.

What changed in GitHub Actions pricing

Until early 2026, private repositories on GitHub received a monthly quota of included minutes depending on the subscribed plan. This model evolved: since March 1, 2026, GitHub reduced the included thresholds and extended per-minute billing to hosted Linux runners on plans below GitHub Team and GitHub Enterprise.

Self-hosted runners have never been billed by GitHub — they consume your own resources. That is exactly the lever Gitea Actions and Forgejo Actions exploit: by hosting your forge on a VPS, you run your pipelines on a runner you control, with no GitHub counter.

The argument goes beyond cost. Your private repository data no longer transits through GitHub's infrastructure. Pipelines run in the network environment of your choice — useful if your deployment targets a private network or an internal cluster. And the forge remains accessible even during external outages or policy changes.

Why migrate your CI/CD to a self-hosted forge

  • Variable cost eliminated — the runner runs on your VPS; each CI minute is an already-paid resource, not an additional billing line.
  • Pipeline confidentiality — source code, environment secrets, and build artifacts never leave your infrastructure.
  • YAML compatibility.gitea/workflows/ci.yml follows the same syntax as .github/workflows/ci.yml; migrating an existing pipeline most often requires only moving a file.
  • Full control over runner images — choose the exact versions of PHP, Node, Python, or Docker without depending on GitHub's catalog.
  • Lightweight forge — Gitea runs on 200 to 300 MB of RAM for repositories; act_runner uses around 50 MB per job; a VPS with 1 GB of RAM is enough for a small team.
  • Service independence — your pipelines keep running regardless of the upstream platform's availability or pricing policy.
  • Unified access management — permissions, teams, and webhooks live on your instance, with no access delegation to a third party.
  • Build artifacts under your control — binaries, Docker images, and coverage reports are stored where you decide.

Prerequisites

Before installing Gitea or Forgejo, verify that your VPS meets the following requirements.

RAM: 1 GB minimum for a small forge (up to 5 developers, simple pipelines). Plan for 2 GB if you enable multiple runners in parallel or if your jobs build Docker images.

CPU: 1 vCPU is enough for the forge itself; CI jobs consume what you allocate through runner configuration.

Storage: 20 GB to start — Git repositories and pipeline artifacts grow quickly depending on your activity.

Public port: port 22 (Git SSH) or an alternative port, and port 443 (HTTPS). Port 3000 is used internally by Gitea/Forgejo and must not be exposed directly.

Domain: a dedicated subdomain (git.your-domain.com) is strongly recommended — GitHub→Gitea webhooks, SSH keys, and clone URLs rely on a stable hostname.

Docker: Gitea and Forgejo deploy cleanly via Docker Compose, simplifying updates and process isolation.

Gitea Actions vs Forgejo Actions vs Woodpecker CI

CriterionGitea ActionsForgejo ActionsWoodpecker CI
OriginFork of Gogs, ~47,000 GitHub stars, MIT licenseHard-fork of Gitea since Dec 2022, managed by Codeberg, AGPL-3.0External CI, open-source, works with Gitea/Forgejo/GitHub
Runneract_runner (same binary)act_runner (same binary, Forgejo version)Dedicated Woodpecker agent (woodpecker-agent)
Workflow syntaxCompatible with `.github/workflows/*.yml`Compatible with `.github/workflows/*.yml`Own YAML syntax, not compatible with GitHub Actions
Migration effortMove the YAML file to `.gitea/workflows/`Move the YAML file to `.gitea/workflows/`Rewrite workflows in Woodpecker format
GovernanceCommercial company (Gitea Ltd)Independent contributor collective (Codeberg e.V.)Community, no commercial entity backing it
Forge memory usage~200-300 MB RAM for repos~200-300 MB RAM for reposAlso requires a forge (Gitea/Forgejo) in addition
UpdatesFrequent releases, stable channel availableReleases aligned with Gitea + own patchesIndependent release cycle
Main use caseLightweight forge with integrated CI, migration from GitHubLightweight forge, integrated CI, preference for open governanceAdvanced standalone CI, complex multi-stage pipelines

Option A: Gitea with act_runner

Gitea is a fork of Gogs that has been actively maintained since 2016, now at approximately 47,000 stars on GitHub under the MIT license. Since version 1.19, it includes an Actions engine compatible with GitHub Actions syntax, driven by act_runner.

Installing Gitea and the runner

01

Create the Docker Compose file

Create a working directory and a docker-compose.yml file:

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__actions__ENABLED=true
    volumes:
      - ./gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    restart: unless-stopped

Enable GITEA__actions__ENABLED=true from the start — without this variable, the Actions tab does not appear in the interface.

02

Launch Gitea and complete initial setup

Start the container then open http://<your-ip>:3000 in a browser. The installation wizard asks for the database type (SQLite is sufficient for a small forge), the server name, and the external URL. Enter the HTTPS URL you will configure (https://git.your-domain.com) — it is written to the configuration and serves as the base for clone URLs and webhooks.

Create the administrator account from this wizard.

03

Configure the reverse proxy and TLS

Place Gitea behind nginx with a Let's Encrypt certificate. Example server block:

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

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

04

Create a runner token in Gitea

Log in to your Gitea instance as an administrator. Go to Site Administration → Runners → Create Runner. Copy the displayed token — it will be used in the next step to register act_runner.

05

Deploy act_runner

Add the act_runner service to your docker-compose.yml:

  act_runner:
    image: gitea/act_runner:latest
    container_name: act_runner
    environment:
      - GITEA_INSTANCE_URL=https://git.your-domain.com
      - GITEA_RUNNER_REGISTRATION_TOKEN=<your-token>
      - GITEA_RUNNER_NAME=vps-runner
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./runner-data:/data
    restart: unless-stopped
    depends_on:
      - gitea

Restart with docker compose up -d. The runner appears in the Gitea interface under Site Administration → Runners with the status Idle.

06

Push a test workflow

In one of your Gitea repositories, create the file .gitea/workflows/ci.yml:

name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check
        run: echo "Pipeline active on Gitea Actions"

Push a commit. The job appears in the repository's Actions tab and runs on your VPS runner.

Option B: Forgejo with act_runner

Forgejo is a hard-fork of Gitea initiated in December 2022 by the Codeberg community, distributed under the AGPL-3.0 license. It shares the same workflow syntax and the same act_runner binary, with community governance and an independent patch cycle. For teams sensitive to governance or licensing questions, Forgejo is the direct alternative to Gitea — the user experience and YAML compatibility are identical.

Installation differences compared to Gitea

01

Replace the Docker image

In your docker-compose.yml, replace the Gitea image with the official Forgejo image:

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:latest
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__actions__ENABLED=true
    volumes:
      - ./forgejo-data:/data
    ports:
      - "3000:3000"
      - "222:22"
    restart: unless-stopped

The environment variable prefix changes from GITEA__ to FORGEJO__ for Forgejo-specific settings.

02

Use the Forgejo runner

Forgejo maintains its own version of act_runner. Use the image published on the Codeberg registry:

  act_runner:
    image: code.forgejo.org/forgejo/runner:latest
    container_name: forgejo_runner
    environment:
      - FORGEJO_INSTANCE_URL=https://git.your-domain.com
      - FORGEJO_RUNNER_REGISTRATION_TOKEN=<your-token>
      - FORGEJO_RUNNER_NAME=vps-runner
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./runner-data:/data
    restart: unless-stopped
03

Retrieve the runner token in Forgejo

The procedure is identical to Gitea: Site Administration → Runners → Create Runner. The token is single-use — note it before closing the page.

04

Place your workflows in `.gitea/workflows/`

Forgejo reads workflow files from the same directory as Gitea: .gitea/workflows/. A workflow written for GitHub Actions or Gitea Actions works without modification. One constraint: the actions/checkout@v4 action and its cousins are resolved by your instance's action cache — the first run downloads them, subsequent runs reuse them.

YAML compatibility with GitHub Actions

The most underestimated compatibility point: .gitea/workflows/ci.yml and .github/workflows/ci.yml share the same grammar. Trigger events (push, pull_request, schedule), jobs, steps, matrices, and if: conditions work the same way.

A typical GitHub workflow:

name: Tests
on:
  push:
    branches: [main, dev]
  pull_request:

jobs:
  phpunit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install --no-interaction
      - run: vendor/bin/phpunit

This file, copied to .gitea/workflows/ci.yml, runs as-is on Gitea Actions or Forgejo Actions. GitHub marketplace actions (actions/checkout, shivammathur/setup-php, etc.) are downloaded from GitHub on first run and cached locally.

Known limitation: some proprietary or GitHub-specific actions (OIDC, CodeQL, Dependabot) have no direct equivalent and must be replaced with open-source alternatives or shell scripts.

Comparative advantages of a self-hosted runner

  • Fixed, predictable cost — you only pay for the VPS, regardless of how often your pipelines run.
  • Confidentiality — source code, environment variables, and build artifacts do not transit through a third-party service.
  • Runner customization — install system dependencies, stack-specific tools, or private Docker images directly on the runner.
  • Internal network access — a job can reach a staging database or a private Docker registry on your network without public exposure.
  • No external network transit — the runner executes in the same datacenter as your target server; build artifacts are copied locally without passing through GitHub servers.
  • Archiving under your control — pipeline logs and artifacts are retained for as long as you decide, with no platform-imposed limit.

Hardening your installation

A self-hosted runner exposes your infrastructure if its configuration is lax. Three points to check before putting your instance into production.

First: do not expose the Gitea/Forgejo admin interface to the Internet. Place it behind the reverse proxy with two-factor authentication enabled and, if possible, an IP restriction or VPN for admin access.

Second: the runner token is single-use and must remain secret. Once the runner is registered, the token has no more value — but if it leaks before registration, a third party can create a malicious runner on your instance.

Third: isolate the runner in a dedicated Docker network, separate from the forge. A compromised job must not be able to reach the Gitea/Forgejo container via the internal Docker network. Declare two networks in your docker-compose.yml and grant the runner only Internet access and what your pipelines need.

Troubleshooting — common errors

The runner stays in 'Offline' status after startup. Check that GITEA_INSTANCE_URL (or FORGEJO_INSTANCE_URL) points to the public HTTPS URL of your forge, not localhost or an internal IP. The runner connects from outside the container.

The Actions tab does not appear in the interface. The variable GITEA__actions__ENABLED=true (or FORGEJO__actions__ENABLED=true) must be present when the container starts. A simple restart without this variable is not enough — stop the container, add the variable, then restart with docker compose up -d.

The actions/checkout@v4 action fails with a resolution error. Gitea and Forgejo try to download actions from GitHub on first run. If your VPS cannot reach github.com, configure a local action mirror in the [actions] section of app.ini using the DEFAULT_ACTIONS_URL key.

The job starts then immediately stops with 'exit code 137'. This is an OOM (Out Of Memory) signal from the kernel. The runner image (ubuntu-latest) loads several tools into memory. Increase the RAM available on your VPS or reduce parallelism (max_parallel_jobs in the runner configuration) to avoid multiple simultaneous jobs on an undersized host.

Take back control of your CI/CD

Gitea and Forgejo offer the same level of compatibility with your existing GitHub Actions workflows, with different governance profiles — MIT for Gitea, AGPL-3.0 and an independent collective for Forgejo. In both cases, act_runner installs in a few minutes on a standard VPS, and your pipelines migrate without rewriting.

If you already use a Gitea VPS template on ServOrbit, the runner can run on the same instance or on a dedicated VPS depending on your pipeline load. The detailed Gitea installation guide is available next — it covers storage options, SMTP configuration, and automated backups.

To deploy Gitea on a VPS in minutes, see our guide: Host Gitea on VPS.

Deploy your Git forge on VPS

Gitea is available as a VPS template on ServOrbit. Your instance is up and running in minutes, with Docker, nginx, and a preconfigured TLS certificate. Add `act_runner` and your pipelines run on your infrastructure from the first hour.

Need help?

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