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.ymlfollows 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_runneruses 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
| Criterion | Gitea Actions | Forgejo Actions | Woodpecker CI |
|---|---|---|---|
| Origin | Fork of Gogs, ~47,000 GitHub stars, MIT license | Hard-fork of Gitea since Dec 2022, managed by Codeberg, AGPL-3.0 | External CI, open-source, works with Gitea/Forgejo/GitHub |
| Runner | act_runner (same binary) | act_runner (same binary, Forgejo version) | Dedicated Woodpecker agent (woodpecker-agent) |
| Workflow syntax | Compatible with `.github/workflows/*.yml` | Compatible with `.github/workflows/*.yml` | Own YAML syntax, not compatible with GitHub Actions |
| Migration effort | Move the YAML file to `.gitea/workflows/` | Move the YAML file to `.gitea/workflows/` | Rewrite workflows in Woodpecker format |
| Governance | Commercial 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 repos | Also requires a forge (Gitea/Forgejo) in addition |
| Updates | Frequent releases, stable channel available | Releases aligned with Gitea + own patches | Independent release cycle |
| Main use case | Lightweight forge with integrated CI, migration from GitHub | Lightweight forge, integrated CI, preference for open governance | Advanced 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
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-stoppedEnable GITEA__actions__ENABLED=true from the start — without this variable, the Actions tab does not appear in the interface.
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.
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.
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.
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:
- giteaRestart with docker compose up -d. The runner appears in the Gitea interface under Site Administration → Runners with the status Idle.
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
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-stoppedThe environment variable prefix changes from GITEA__ to FORGEJO__ for Forgejo-specific settings.
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-stoppedRetrieve 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.
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/phpunitThis 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.