I’ve been self-hosting a Gitea instance (previously Gogs) since roughly 2017. Since Gitea has historically been a very minimal Git hosting solution (note: nowadays it is very featureful!), it did not come with a builtin CI/CD solution (for testing, building, packaging, etc.). Thus I was also hosting a Drone instance to go along with it, which I’ve been using for my self-hosted, continuous blog deployment (among other projects).
Drone was the first CI/CD solution that can be described as “container native”: by default, all jobs are executed within a container, and container images are the basic building block for CI pipelines. I was really impressed by this approach and since then it has been ingrained into my brain as a best practice.
Unfortunately, at some point the Drone source code was relicensed from Apache 2 to a proprietary license. This lead some community members to fork the last open source version into a new project called Woodpecker (read more about the details here)
Recently, Gitea introduced built-in support for “Actions”, their solution for a CI/CD system that is very closely modeled after GitHub Actions (like the rest of Gitea). This brings the major benefit that the large number of guides, tutorials and examples that are out there for GitHub Actions (which has seen rapid adoption since its introduction in 2018) can be used for Gitea Actions - this applies especially to the thousands of “Actions” (preconfigured containers that can be used as part of CI workflow) out there.
In addition, an operational benefit is that now I need to maintain and update only one service (Gitea) instead of two (Gitea + Woodpecker).
This post will go over how to enable Gitea Actions, how to set up an Actions Runner (with Podman, Quadlet and systemd) and finally how I converted the CI pipeline for my blog to the new format.
# Enable Gitea Actions
Gitea Actions was initially introduced in 1.19 and has received rapid improvements and bug fixes in the following releases. Therefore it is generally recommended to install the latest stable release, but at least 1.20 to get a smooth experience.
As of 1.20, the Gitea Actions feature is not enabled out-of-the-box (see Gitea Actions FAQ).
The following configuration parameter needs to be set in the
If you are using the Gitea Helm chart (like me), you can enable it by adding the following to your
Roll out the
helm upgrade and we’re ready to go.
# Setup Actions Runner
The runner is the component that takes care of executing the commands that make up the CI/CD pipeline, whereas the main Gitea server is just responsible for orchestrating the various pipelines and jobs (triggering on events, assigning jobs to runners, providing the UI etc.). Other CI/CD solutions call this component agent or worker.
The official runner supported by Gitea is called act_runner. Several deployment examples for various different platforms are available. For security and performance reasons, I’m hosting the runner inside a separate and minimal virtual machine.
I decided to take this opportunity play around with the Quadlet: a translation layer that natively integrates Podman with systemd.
The gist of it is that container description files (which look very similar to systemd unit files) can be dropped into
/etc/containers/systemd/* (see this description of all available options), which are then automatically translated into systemd units containing the Podman commands for necessary starting and stopping the described container as a service unit.
<gitea-hostname> with the hostname of your Gitea instance (in my case
When the runner launches for the first time, it uses a one-time token to register itself to the Gitea instance.
After successful startup, the registration information is stored in
/data/.runner, therefore this directory must be persisted: for this purpose the unit manifest mounts a Podman Volume to that directory (line 10).
To generate the a registration token for a runner available to all repositories on the Gitea instance, log in as an administrator on Gitea, switch to the Site Administration page (1), open the Runners section (2) and click on the
Create new Runner button (3) - copy the token and replace it in the systemd file.
You can find more details about the act_runner in the Gitea documentation.
Sidenote: from an administrator point of view I find the design quite unfortunate, because it means that the runners are a stateful (they store some state locally which must be persisted) and since the registration token can only be used once, there is no way to use it in a reproducible infrastructure-as-code environment.
The unit also makes the Podman socket available inside the container (line 9) so the
act_runner (inside the container) can spawn new containers on the host (aka. “docker-in-docker”).
Note that we pass the hostname of the machine onto the container (line 8) so we can easily identify the runner from Gitea web UI - otherwise, the container will get a random hostname like
Systemd will replace the
%H by the hostname when the service starts, see systemd unit specifiers.
To test if the unit file works as expected, the Quadlet’s dry-run option can be used:
Now we’re ready to launch the systemd service.
Note that the logs can be viewed with
podman logs my-runner or
journalctl -u my-runner.
# Enable Gitea Actions for the repository
While we have enabled the Gitea Actions feature in the first section, the setting still needs to be enabled for each repository that wants to use it. Go to the repository settings (1) and check the option Enable Repository Actions (2) - don’t forget to save the new settings (3).
Afterwards, a new tab called Actions should appear in the repository:
# Convert pipeline definition
I won’t dive into too much detail here, instead I’ll just show the before and after. Refer to GitHub Actions Quickstart for a full-blown introduction to the pipeline manifest syntax and available options. The most significant difference between the two pipelines is that previously (with Woodpecker) I used an extra build step (and container image) for uploading the generated artifacts (HTML, CSS etc.). Now (with Gitea Actions) I’m using Hugo’s built-in deploy feature that does exactly the same.
Woodpecker CI pipeline manifest (
Gitea Actions pipeline manifest (
Unlike other CI solutions (e.g. Gitlab CI), secrets are not directly exposed as environment variables, instead they can be accessed through the
secrets context (lines 19-22).
In my opinion this is a good design because it avoids accidentally exposing credentials (for example when running
env) and prevents unintended conflicts between variables set from secrets and from the environment.
Secrets can be added through the repository settings (1) in the tab Secrets (2).