Self-Hosted CI/CD with Gitea Actions: Build, Test and Deploy from Your Own Server

2026-11-05 · Unix3r

GitHub Actions workflows, running entirely on your own infrastructure. How to enable Gitea Actions, register a runner in Docker, reuse the GitHub Actions ecosystem, and ship a real build-and-deploy pipeline that pushes to your servers over Tailscale

If you already self-host Gitea, you're one container away from self-hosted CI/CD. Gitea Actions is almost fully compatible with GitHub Actions workflow syntax, runs the same marketplace actions, and the runner is a single binary. No Jenkins, no Drone, no YAML dialect to learn — the workflows you'd write on GitHub run here unchanged.

This post sets it up end to end: enable Actions, register a runner, and build a pipeline that lints, tests, builds a Docker image, pushes it to Gitea's built-in registry, and deploys it to a server over Tailscale.

1. Enable Actions on the Gitea server

DEFAULTACTIONSURL = github means uses: actions/checkout@v4 resolves to github.com, so the whole GitHub Actions ecosystem works. If your Gitea can't reach GitHub, mirror the actions you need into a Gitea org and set this to self.

Restart Gitea. Then Site Administration → Actions → Runners → Create new runner and copy the registration token.

2. Run the runner in Docker

The runner needs Docker to run jobs in containers, so it gets the socket. Put it on a box with spare CPU — not the Gitea server itself, if you can help it.

The runner shows up under Site Administration → Actions → Runners as Idle.

Labels are how workflows pick an image. runs-on: ubuntu-latest maps to Gitea's own runner-images:ubuntu-latest, which has Node, Python, Docker CLI, and the usual tools pre-installed — close enough to GitHub's that most actions just work.

3. Secrets

Repository or organization level, like GitHub: Settings → Actions → Secrets. For the pipeline below, add:

| Secret | What |
|---|---|
| DEPLOYSSHKEY | Private key of a dedicated deploy key on the target server |
| DEPLOYHOST | Target's Tailscale IP, e.g. 100.64.10.5 |
| DEPLOYUSER | deploy |
| NTFYTOKEN | For success/failure pushes (optional) |

You don't need a registry secret: Gitea injects GITHUBTOKEN (and the alias GITEATOKEN) into every job, and it can push to the built-in package registry.

The runner box must be on the same tailnet as the deploy targets. Tailscale inside the runner container isn't needed — the job containers use the host network path through the Docker bridge, so if the host can reach 100.64.10.5, so can the job.

4. The pipeline

Notes on what differs from GitHub:

The context is gitea. rather than github. — but github. also works as an alias, so copied workflows don't break.
Workflows live in .gitea/workflows/. .github/workflows/ is also picked up, which is handy for repos mirrored from GitHub.
GITHUBTOKEN has write access to the repo's packages, so the registry login needs no extra secret.

On the target server, the compose file references the image by its registry path:

And the deploy user has done docker login git.example.com once with a personal token that has read:package scope, so docker compose pull works outside CI too.

5. Caching

Two caches matter and both are configured above:

actions/setup-node with cache: npm uses the runner's cache server (the cache: block in config.yaml). First run is slow; subsequent runs restore nodemodules sources in seconds.
BuildKit registry cache (cache-from/cache-to) stores Docker layer cache as an image tag in Gitea's registry. Rebuilds of an unchanged base take 10 seconds instead of minutes.

If cache restores never hit, check that the runner container can reach itself on the cache port — set cache.host to the runner host's Docker bridge IP (172.17.0.1) explicitly.

6. Keep the runner healthy

Prune regularly. Job containers and build cache accumulate. A weekly docker system prune -af --filter "until=168h" on the runner host keeps the disk sane.
Pin the runner image to a version once it works (gitea/actrunner:0.2.11), and upgrade deliberately. The runner and Gitea versions need to be in the same rough era.
One runner per box, capacity ≤ CPU cores / 2. Builds are bursty; oversubscribing makes every job slow.
Monitor it. An Uptime Kuma Docker-container monitor on act-runner, and a push-heartbeat monitor hit by a trivial scheduled workflow (on: schedule: cron: "0 ") that curls the heartbeat URL — so you know when the runner has quietly stopped picking up jobs.

When Gitea Actions isn't enough

It has no built-in matrix of OS images beyond Linux containers, no macOS/Windows runners, and the UI for logs and artifacts is simpler than GitHub's. For a homelab, a side project, or a small team that's a fine trade for keeping your source, your builds, and your deploy keys on a machine you own. For everything else, the workflows are portable — move them back to GitHub by renaming the folder.