- Add instance-scoped base data/config directories in defaults - Create gitea-runner@.service.j2 template supporting Docker and binary modes - Refactor service.yml to install systemd template unit instances - Remove direct container lifecycle from docker_mode.yml (delegate to systemd) - Add deregister.yml for runner deregistration on disable/remove - Create lifecycle playbooks: start, stop, enable, disable, status, remove - Update handlers, integration_test, docker_update, binary_update for template units
Gitea Runner Manager (GRM)
A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts.
By default, runners are deployed as Docker containers using the official gitea/runner image. A traditional binary deployment mode is also available.
Pronunciation note: GRM is short for Gitea Runner Manager, but say it like ГРЪМ (roughly "GRUM" in Latin letters) — the Bulgarian word for thunder. Wherever there are clouds, there may be thunders. This is an open-source project from Oblachno (облачно means cloudy in Bulgarian).
Features
- Simple and focused — no unnecessary features.
- Secure — no hardcoded secrets, uses scoped tokens.
- Idempotent — can be run multiple times safely.
- Flexible — accepts a plain IP address or hostname, and allows specifying the SSH user and private key.
Supported Operating Systems
- Arch Linux
- Ubuntu 22.04 / 24.04 / 26.04
- Debian 12 / 13
Prerequisites
- SSH key authentication — The remote host must be reachable via SSH using the user specified with
--userand the private key specified with--key. GRM uses Ansible under the hood, which connects to the target host over SSH to execute all installation and configuration tasks. Without valid SSH credentials, Ansible cannot establish a connection and the deployment will fail. - Sudo privileges — The specified user must have passwordless or password-prompted (
--ask-become-pass) sudo access on the remote host. Runner installation requires root privileges for tasks such as installing packages, creating systemd services, and managing Docker.
Quick Start
Developer Setup
git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git
cd gitea-runner-manager
pyenv install 3.11.11
pyenv local 3.11.11
make setup
Configure Gitea Credentials
cp .env.example .env
# Edit .env:
# GITEA_URL=https://git.example.com
# GITEA_REGISTRATION_TOKEN=your-registration-token
GITEA_REGISTRATION_TOKEN is the runner registration token obtained from your Gitea instance (Admin → Actions → Runners → Create Registration Token).
Admin API Token (optional)
Set GITEA_ADMIN_TOKEN to enable informational API checks during integration test. This is optional — the test primarily verifies the runner by checking:
.runnerregistration file exists and contains valid JSON (proves successful registration)- Container/service is running (proves daemon is polling for jobs)
API checks, if enabled, are purely informational and do not affect pass/fail.
Install a Runner
Using the CLI:
# Docker mode (default) — deploys gitea_runner as a container
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
# Binary mode — downloads and installs the gitea_runner binary with systemd
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --mode binary
If the remote user requires a sudo password:
grm install 192.168.1.10 --user ubuntu --ask-become-pass
Using Make:
# Docker mode (default)
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
# Binary mode
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner MODE=binary
# With sudo password prompt:
make install HOST=192.168.1.10 USER=ubuntu ASK_BECOME_PASS=1
Verify Runner
The installer performs an automated integration test that verifies:
.runnerfile exists with valid JSON containingid,uuid,token,address— this proves successful registration with Gitea- Container/service is running — this proves the daemon is active and polling for jobs
You can also check the Gitea UI under Actions → Runners to confirm the runner appears as Online.
Optional: If GITEA_ADMIN_TOKEN is set, the installer will also query the Gitea API and report whether the runner appears in the admin or repo runners list. This is purely informational.
View Logs
# Binary mode logs
sudo journalctl -u gitea-runner-<name> -f
# Docker mode logs
docker logs gitea-runner-<name> -f
Architecture
GRM consists of two layers:
- Python CLI (
src/gitea_runner_manager/) — built with Click, handles argument parsing, environment loading, and delegates to Ansible via theansible-playbooksubprocess. - Ansible Role (
ansible/roles/gitea-runner/) — idempotent role that installs Docker, configures the runner (binary or container), creates systemd services, and registers the runner with Gitea.
grm install <host>
└── RunnerManager.install()
└── ansible-playbook ansible/install-runner.yml
└── role: gitea-runner
├── docker.yml (Docker installation)
├── docker_mode.yml (container deployment)
├── binary_mode.yml (binary + systemd deployment)
├── prune.yml (Docker prune timer)
└── integration_test.yml (validate online status)
Configuration
All tunable values are exposed as Ansible variables in ansible/roles/gitea-runner/defaults/main.yml:
| Variable | Default | Description |
|---|---|---|
runner_mode |
docker |
Deployment mode: docker or binary |
gitea_runner_version |
1.0.8 |
Docker image tag / binary version |
gitea_runner_docker_image |
gitea/runner |
Docker image name |
runner_labels |
ubuntu-latest:docker://runner-images:ubuntu-22.04 |
Runner labels |
skip_runner_registration |
false |
Skip API registration (useful for tests) |
gitea_runner_data_dir |
/var/lib/gitea-runner |
Runtime data directory |
gitea_runner_config_dir |
/etc/gitea-runner |
Config directory (binary mode) |
gitea_runner_binary_path |
/usr/local/bin/gitea_runner |
Binary install path |
gitea_runner_prune_until |
24h |
Prune resources older than this |
gitea_runner_prune_schedule |
daily |
systemd timer schedule |
gitea_runner_prune_label |
gitea-runner=true |
Docker label for pruning |
gitea_runner_service_restart_sec |
5 |
systemd RestartSec value |
gitea_runner_service_user |
{{ ansible_user | default('root') }} |
Service user |
gitea_runner_log_level |
info |
Runner log level |
gitea_runner_container_label |
gitea-runner=true |
Container label |
docker_gpg_key_path |
/etc/apt/keyrings/docker.asc |
Docker GPG key path |
Override any variable by passing it to the CLI with --extra-vars or by setting it in your Ansible inventory.
Development
Project Structure
.
├── src/gitea_runner_manager/ # Python CLI source
│ ├── cli.py # Click commands
│ ├── runner_manager.py # Ansible orchestration
│ └── exceptions.py # Custom exceptions
├── ansible/
│ ├── roles/gitea-runner/ # Main Ansible role
│ │ ├── defaults/main.yml # Default variables
│ │ ├── tasks/ # Task files
│ │ ├── templates/ # Jinja2 templates
│ │ └── molecule/ # Test scenarios
│ ├── install-runner.yml # Install playbook
│ └── update-runner.yml # Update playbook
├── tests/ # Unit tests
├── Makefile # Build & test automation
└── pyproject.toml # Python project metadata
Setup Development Environment
make setup # Creates venv, installs deps, sets up hooks
source .venv/bin/activate
Running Linters
make lint # Python (ruff + pyright)
make ansible-lint # Ansible
make makefile-lint # Makefile
Testing
Unit Tests
make test-unit
Runs pytest with 100% coverage requirement.
Molecule Tests
make molecule
Runs two scenarios:
- default — Docker mode installation in an Ubuntu 22.04 container
- binary — Binary mode installation in an Ubuntu 22.04 container
Both scenarios test idempotence (second run produces zero changes).
Full Test Suite
make test-all # Runs unit tests + linters + molecule
Troubleshooting
"Event loop is closed" warning
This is a harmless cleanup traceback from Molecule's Docker driver when the test process is interrupted. It does not indicate a test failure.
Runner appears offline after installation
- Check that the
GITEA_URLandGITEA_REGISTRATION_TOKENenvironment variables are correct. - Verify the runner container or service is running:
docker psorsystemctl status gitea-runner-<name>. - Check logs for registration errors.
Integration test fails
The test checks two things:
-
.runnerfile missing or invalid — Registration failed. Check:GITEA_URLandGITEA_REGISTRATION_TOKENare correct- Runner logs for registration errors
- The
.runnerfile should exist at/var/lib/gitea-runner/.runner(Docker) or/etc/gitea-runner/.runner(binary)
-
Container/service not running — Daemon failed to start. Check:
docker psorsystemctl status gitea-runner-<name>- Logs for connection errors
Docker mode: container won't start
- Ensure Docker is installed and running on the host.
- Verify the Docker socket is accessible:
docker version.
Binary mode: systemd service fails
- Check the service status:
systemctl status gitea-runner-<name>. - Verify the binary exists at
gitea_runner_binary_path. - Ensure the service user is in the
dockergroup.
Makefile Targets
| Target | Description |
|---|---|
setup |
Full environment setup |
install |
Installs a runner on a host (MODE=docker or binary) |
update |
Updates a runner on a host (MODE=docker or binary) |
lint |
Runs Python linters |
ansible-lint |
Runs ansible-lint |
test-unit |
Runs unit tests with coverage |
molecule |
Runs Ansible Molecule tests |
test-all |
Runs all tests |
License
GPL-3.0