CI/CD with Gitea in HomeLab
published: ⏰ 7I’ve rarely encountered Gitea as a DVCS + CI/CD solution; everyone usually uses GitLab. In fact, I tried using GitLab myself first, but I ran into problems almost immediately. Its main problem was its appetite for limited resources. So I had to look for alternatives, and Gitea turned out to be one of them.
Introduction
Why is all this necessary?
Firstly, one way or another, for normal operation (SDLC), at least some kind of established process for automating the build, testing, and delivery of applications is required. Doing this manually is, to put it mildly, inefficient and unscalable.
Secondly, it’s not always possible to use public servers like GitHub or GitLab. Although they have private repositories, there’s always the risk of code leaks. Or the service itself might be blocked, and you’ll have to restore everything from backups—you’ll be lucky if they were made frequently enough.
Thirdly, sometimes you want complete control over your data and processes.
That’s why I started looking for a DVCS server and CI/CD for it when I needed to deploy a basic development environment.
Options
Most projects typically required fairly limited functionality:
- Code storage.
- CI/CD.
- MR/PR.
- Sometimes a task tracker.
For this reason, there were no special requirements for the DVCS server. Among the well-known free solutions that could be deployed locally, there were several options:
| Solution | CI/CD | Setup Complexity | Development Language | License |
|---|---|---|---|---|
| GitLab | built-in | High | Ruby | Apache 2 |
| Gitea | built-in, Drone, Woodpecker, Tekton | Low | Go | MIT |
| Forgejo (Gitea fork) | Woodpecker | Low | Go | GPLv3 |
| GitBucket + Jenkins | Jenkins | Medium | Scala | Apache 2 / MIT |
To be honest, I didn’t try anything beyond Gitea, as it was completely suitable. And judging by the screenshots, they have pretty similar setups.
GitLab and Gitea
What was the actual problem with GitLab? The functionality is clear: it’s widely used, has a lot of features, and supports a lot. But the startup speed and resource consumption were an unpleasant surprise. Below, as an example, are empty servers with several projects and a couple of users.
Gitlab’s processes and memory consumption are ~7.5 GiB, with a startup time of 2-3 minutes:

The startup time is a couple of minutes (before accessing the UI) and puts quite a bit of CPU load when launching all the services. It seems possible to disable clustering (Puma) in GitLab, which will reduce memory consumption, but at the cost of losing parallelism.
Gitea processes and memory consumption are ~200-300 MiB, startup time is 2-5 seconds:

Setting up CI/CD runners for Gitea and Git is roughly the same and simple. Runners for both can be deployed in Docker or on the host. Launch, register, and you’re ready to build projects:
The differences begin here; their pipelines are different:
- GitLab uses its own CI/CD pipelines description format.
- Gitea uses a fork of nektos/act, which is an implementation of GitHub Actions for local execution.
It’s hard to say which is better or worse. I feel like GitLab is more functional out of the box, while Gitea is more intuitive. Gitea also allows you to create your own actions, which can then be used in pipelines. And we can also use existing GitHub Actions; all the necessary ones were available.
Example of the Gitea pipeline used for testing:
name: Build
on:
pull_request:
branches:
- main
- develop
push:
branches:
- main
- develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Maven
uses: stCarolas/setup-maven@v5
with:
maven-version: '3.9.9'
- name: Cache local Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache Node.js modules
uses: actions/cache@v4
with:
path: ~/.npm # Or ./node_modules if you prefer caching locally
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Set up JDK (21)
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Set up Node.js (24)
uses: actions/setup-node@v4
with:
node-version: 24
- name: Setup settings.xml
uses: s4u/maven-settings-action@v3.1.0
with:
mirrors: |
[{
"id": "nexus.public",
"name": "public",
"mirrorOf": "*",
"url": "http://${{ vars.NEXUS_HOST }}/repository/maven-public/"
}]
servers: |
[{
"id": "nexus.private",
"username": "${{ secrets.NEXUS_USER }}",
"password": "${{ secrets.NEXUS_PASS }}"
}]
- name: Build and test
run: mvn --batch-mode clean deploy -Dnexus.host=${{ vars.NEXUS_HOST }}
- name: Generate JaCoCo coverage and Sonar reports
run: mvn --batch-mode jacoco:report sonar:sonar -Dsonar.token=${{ secrets.SONAR_TOKEN }} -Dsonar.host.url=${{ vars.SONAR_HOST }} -P sonarThis script performs the following steps:
- Obtaining code.
- Configuring Maven/NPM caching.
- Installing Maven.
- Installing NPM.
- Installing the JDK.
- Building the project and deploying artifacts to the repository.
- Generating test reports.
- Running Sonar.
It’s quite fast, considering the average project size (65 modules, ~41k lines) has:
- Protobuf and Antlr4 generators.
- Backend (Spring Boot).
- Frontend (NodeJS).
- SAST, tests, reports, etc.

And a couple of screenshots of the interface, very similar to GitHub.


Possible Improvements
Considering that switching from GitLab to Gitea saved almost 8 GiB of RAM, I decided to experiment with building in RAM (tmpfs). As a bonus, it also reduced IOPS and SSD wear. By default, the runner (Docker) doesn’t allow you to mount the working directory as tmpfs. All volumes are mounted on the local file system inside the task’s temporary container. The only option is to run it on the host, without Docker. Another option is to set up a VM and do everything there, but I wasn’t very comfortable with that.
It’s not particularly difficult, but I had to fork two projects:
- https://github.com/devmix/fork-gitea-act – this is a modified fork of
nektos/act, which needed to add support for tmpfs mount options for containers. - https://github.com/devmix/fork_gitea_act_runner – this is the runner itself, which is used in Gitea Actions.
The only issue was with dependencies and updating the Docker module to a version that supports tmpfs options. By
default, the noexec flag is set, and the protobuf compiler stops working when building the project. Eventually,
everything worked fine. And it can be configured with a couple of parameters via the runner configuration.
container:
workdir_mount_as_tempfs: true
workdir_tempfs_options: [ [ exec ] ]Server
Why am I trying to conserve resources? I’ve decided to deploy the entire environment on a separate machine without virtual machines for now. This means the bare minimum required configuration consists of:
- DVCS (GitLab → Gitea)
- CI/CD (GitLab → Gitea)
- Artifact repositories: Maven, NPM, Docker Registry (Nexus)
- Shared RDBMS (PostgreSQL)
- SAST (SonarQube)
- NFS (Samba)
- Issue tracker (YouTrack → Gitea)
- Mail server (poste.io)
- LLM agents and projects themselves
Everything is on Docker Compose, but perhaps Proxmox/ESXi would have been better suited for deploying individual VMs. But then again, the question is how to slice them with minimal overhead.
Another issue with low WiFi speeds surfaced. Perhaps the router, which is 11 years old, can’t handle it, or the airwaves are clogged (changing channels doesn’t help). I couldn’t get more than 8 MiB/s when transferring files. Ultimately, I bought a switch with 2.5G ports and connected everything to it. I had to revisit the old days of crimping patch cords, but fortunately, without installing conduits. My hands are still sharp :).
I actually thought I’d have problems with the old 5e cat cable and wouldn’t get the speed I needed. And the cable had been sitting around for 11 years, too. But no, the speed between devices is at the theoretical maximum for 2.5G.
Accepted connection from fe80::ccce:ac20:594c:7b73, port 58386
[ 5] local fe80::ecdf:64b9:2ebd:1c port 5201 connected to fe80::ccce:ac20:594c:7b73 port 49018
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 1.00-2.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 2.00-3.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 3.00-4.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 4.00-5.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 5.00-6.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 6.00-7.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 7.00-8.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 8.00-9.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 9.00-10.00 sec 277 MBytes 2.32 Gbits/sec
[ 5] 9.00-10.00 sec 277 MBytes 2.32 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 5] 0.00-10.00 sec 2.82 GBytes 2.42 Gbits/sec receiver This ExeGate EXE-725C-45 also turned out to be useful. It’s inexpensive and needed for a laptop without an RJ45 port. The speed is exactly what’s shown in the table above.
The SFP+ ports could be used for a NAS in the future, if I do manage to build one.

Conclusion
What can I say, now I’m using Gitea. I need more time for testing, but for now I like GitLab better and the interface seems more user-friendly.
Based on resource consumption, the switch was definitely worth it. Looking at scalability, Gitea’s public hosting platform has tens of thousands of projects and users, and it seems to scale.