5.7 KiB
Running CI on a self-hosted Gitea
The workflows in .gitea/workflows/ mirror the GitHub ones in
.github/workflows/, with one structural difference: releases build on a
single Linux runner rather than three.
That is not a compromise. packaging/build.py normalises everything that would
otherwise vary by build host — tar member modes, uid/gid, and launcher line
endings — so the Windows .zip produced on Linux is equivalent to one produced
on Windows. Dropping the matrix also drops the artifact hand-off between jobs
and the race of three jobs racing to create the same release.
Multi-OS runners would still be worth adding for testing. Building does not need them.
1. Enable Actions
In app.ini:
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://github.com
DEFAULT_ACTIONS_URL is where actions/checkout and friends are fetched from.
Public repositories clone anonymously, so a locked or absent GitHub account does
not affect this. Restart Gitea afterwards.
2. Register a runner
Get a registration token from Site Administration → Actions → Runners, then:
docker run -d --restart always --name gitea-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/gitea-runner:/data \
-e GITEA_INSTANCE_URL=https://git.example.lan \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
-e GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-22.04 \
gitea/act_runner:latest
The label mapping is the part that matters. runs-on: ubuntu-latest in the
workflow resolves through it to a container image. catthehacker/ubuntu images
carry the tooling GitHub's hosted runners have; the bare ubuntu:22.04 image
does not, and actions/setup-python will fail on it for want of basic
utilities.
Mounting the Docker socket lets the runner start job containers. It also grants effective root on the host, which is the usual trade — acceptable on a machine that only serves this purpose, worth knowing about regardless.
3. Add the release token
Releases are published through Gitea's own API by
packaging/publish_gitea.py, so the job needs a token:
- Settings → Applications → Generate New Token, scope
write:repository. - In the repository: Settings → Actions → Secrets → Add Secret, named
GITEA_TOKEN.
GITEA_URL and GITEA_REPOSITORY come from github.server_url and
github.repository, which Gitea populates for compatibility. Nothing else to
configure.
4. If the runner cannot reach Gitea
Worth understanding before it bites, because the failure is confusing.
ROOT_URL is what Gitea advertises to the world, and job containers inherit it
as github.server_url — so actions/checkout and publish_gitea.py both call
the public address, even though the runner may be sitting on the same host as
Gitea. When that address only resolves outside your network, jobs fail at
checkout with a DNS or connection error while Gitea itself is plainly fine.
Two ways out.
Let it hairpin. If your router does NAT loopback, the request leaves and comes straight back to the reverse proxy, and nothing needs configuring. Try this first — it is the common case and costs nothing to test.
Pin the name to a local address. Map the public hostname to your reverse proxy's LAN address, on the runner and on the containers it spawns:
services:
gitea-runner:
extra_hosts:
- "git.example.lan:192.0.2.10" # the reverse proxy, not Gitea
and in act_runner's config.yaml, so job containers inherit it:
container:
options: --add-host=git.example.lan:192.0.2.10
The trap is which host to point at. It must be whatever terminates the scheme
in ROOT_URL. If ROOT_URL is https://… then the override has to reach
something serving TLS on 443 — your reverse proxy. Pointing it straight at the
Gitea host sends an HTTPS request to a port where Gitea is only speaking plain
HTTP, and the connection is refused.
Also: do not put Gitea behind a forward-auth proxy (Authentik outposts, oauth2-proxy and friends). Runners authenticate with their own token and cannot satisfy an SSO interstitial, so registration and every job will fail. Use the identity provider as an OIDC source inside Gitea's own login instead.
5. Cut a release
Dry run first — workflow_dispatch builds all three archives and prints their
sizes without creating anything:
Actions → release → Run workflow
Then tag:
git tag v2.0.0 && git push origin v2.0.0
That leaves a draft release with the three archives attached. Review, then publish from the web UI. Re-running the workflow on the same tag replaces the assets rather than duplicating them, so a half-finished publish is safe to retry.
To publish immediately instead of drafting, add --publish to the
publish_gitea.py invocation in .gitea/workflows/release.yml.
Notes for a DL380p Gen8
Those E5-2600 v1/v2 chips are x86-64-v2 — they have AVX but not AVX2, which arrived with the v3 (Haswell) generation. Two consequences:
-
Pick the distribution with that in mind. Debian, Ubuntu and RHEL 9 target a baseline these CPUs meet. RHEL 10 and recent Fedora raised their floor to x86-64-v3 and will not run. Node 22 and Docker are fine either way.
-
Don't plan to run benchmarks on this box. Many prebuilt
llama-cpp-pythonwheels assume AVX2, and the ones that don't will be slow on 2012 silicon regardless. It is a fine CI and git server; keep inference on the workstation. Nothing intests_py/needsllama-cpp-python, which is exactly whyrequirements-ci.txtomits it.
Two dual-socket Sandy/Ivy Bridge sockets have plenty of throughput for Gitea plus a runner — this is an I/O and RAM workload, not a vector-math one.