Table of Contents
- FAQ
- How to obtain the Gitea registration token
- What is the CI_GITEA_TOKEN and is it needed?
- How to skip the sudo password prompt for automation
- Can multiple runners run on the same host?
- Why does a runner appear offline after installation?
- What does the "Event loop is closed" warning mean?
- Where are runner connection details stored?
- How to update the gitea_runner binary
- How to remove a runner entirely
- What is the difference between disable and remove?
- What operating systems are supported?
- How to change the UI language
- How to enable debug logging
- What runner labels should be used?
- Is GRM secure?
- Can GRM be installed via pip?
- How does GRM handle idempotence?
FAQ
How to obtain the Gitea registration token
There are three levels of registration tokens, depending on which repositories the runner should serve:
- Instance-level — Site Administration → Actions → Runners → Create Registration Token. The runner will handle jobs from all repositories.
- Organization-level — Organization → Settings → Actions → Runners → Create Registration Token. The runner will only handle jobs from repositories in that organization.
- Repository-level — Repository → Settings → Actions → Runners → Create Registration Token. The runner will only handle jobs from that specific repository.
Set the token as GITEA_REGISTRATION_TOKEN in your .env file or pass it via --token on the command line.
What is the CI_GITEA_TOKEN and is it needed?
CI_GITEA_TOKEN is a Gitea admin API token used for optional post-install verification. When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational — the integration test passes/fails based on the .runner file and systemd service, not the API check.
To generate one: Settings → Applications → Generate New Token, with the admin scope (or at minimum read:user, read:repository, read:admin).
If you skip it, GRM will still verify the runner correctly — it won't show the extra API confirmation.
How to skip the sudo password prompt for automation
Configure passwordless sudo on the remote host and pass --no-ask-become-pass to the CLI command. This is recommended for CI/CD pipelines.
On the remote host, add a sudoers entry:
echo "ubuntu ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/grm
Then use:
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --no-ask-become-pass
Can multiple runners run on the same host?
Yes. Each runner instance is fully isolated with its own system user (grm-<name>), rootless Docker daemon, data directory, and systemd user service. Install additional runners with different --name values and manage them independently by name.
grm install 192.168.1.10 --user ubuntu --name workflow-runner
grm install 192.168.1.10 --user ubuntu --name build-runner
grm list
Runners on the same host never interfere with each other or with the host's Docker installation.
Why does a runner appear offline after installation?
Check that GITEA_URL and GITEA_REGISTRATION_TOKEN are correct, verify the runner service is running with sudo -u grm-<name> systemctl --user status gitea-runner, and check the logs for registration errors. You can also confirm the runner appears as Online in the Gitea UI under Actions → Runners.
Common causes:
- Registration token expired — generate a new one from Gitea
- Network connectivity issue between the runner host and Gitea
- Rootless Docker daemon not running — check
sudo -u grm-<name> systemctl --user status docker - Lingering not enabled — check
loginctl show-user grm-<name> | grep Linger
What does the "Event loop is closed" warning mean?
This is a harmless cleanup traceback from Molecule's Docker driver when the test process is interrupted. It does not indicate a test failure.
Where are runner connection details stored?
GRM stores each runner's connection details (host, user, SSH key, Gitea URL, labels) in a local JSON registry at ~/.local/share/grm/runners.json. After installation, lifecycle commands work by runner name only — you can override any stored value by passing the corresponding flag.
How to update the gitea_runner binary
Use the grm update command:
grm update 192.168.1.10 --user ubuntu
To update to a specific version:
grm update 192.168.1.10 --user ubuntu --version 1.0.8
The update command downloads the new binary and replaces the existing one at /usr/local/bin/gitea_runner. The runner service is restarted automatically.
How to remove a runner entirely
Use the grm remove command:
grm remove prod-runner --token <registration-token>
This deregisters the runner from Gitea, stops and disables the systemd service, removes the system user, deletes data and config directories, removes subuid/subgid entries, disables lingering, and removes the entry from the local registry.
If the remote host is already gone or unreachable, use --force to skip remote cleanup and only remove the local registry entry:
grm remove prod-runner --force
What is the difference between disable and remove?
grm disable <name>— Deregisters the runner from Gitea and stops the service, but leaves the user, directories, and service files in place. The runner can be re-enabled later withgrm enableand re-registered with a new token.grm remove <name>— Removes the runner entirely: deregisters from Gitea, stops and disables the service, removes the system user, deletes all directories, and removes the local registry entry. This is irreversible.
What operating systems are supported?
GRM supports Arch Linux (rolling), Ubuntu 22.04/24.04, and Debian 12. All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files.
On Arch Linux, the rootless Docker setup scripts (dockerd-rootless-setuptool.sh, dockerd-rootless.sh) and the rootlesskit runtime are provisioned automatically — Arch's docker package does not ship them. No extra setup is required.
How to change the UI language
Set the GRM_LANG environment variable to one of the supported languages: en (English, default), bg (Bulgarian), de (German), ru (Russian), zh (Chinese), pl (Polish).
GRM_LANG=bg grm install 192.168.1.10 --user ubuntu --name prod-runner
Or set it in your .env file:
GRM_LANG=bg
How to enable debug logging
Set the GRM_LOG_LEVEL environment variable to DEBUG:
GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner
The log file at ~/.local/state/grm/logs/grm.log always captures DEBUG level regardless of this setting. Ansible execution logs are stored in timestamped files at ~/.local/state/grm/logs/ansible-<timestamp>.log.
What runner labels should be used?
By default, runners are registered with docker,ubuntu-latest:docker://runner-images:ubuntu-22.04. You can override this with --labels or the GITEA_RUNNER_LABELS environment variable.
Use an official Gitea runner image with Node.js, Python, and Docker CLI. Avoid bare OS images like alpine:latest because actions/checkout@v4 needs Node.js.
Example:
grm install 192.168.1.10 --user ubuntu --name prod-runner \
--labels "docker:docker://gitea/runner-images:ubuntu-latest"
Is GRM secure?
Yes. GRM is designed with security as a first-class concern:
- Rootless Docker: Each runner operates under a dedicated unprivileged system user. Containers never have root access to the host.
- Secret handling: Registration tokens are passed via temporary JSON files with
0600permissions, never on the command line (CWE-214). - No shell injection: The CLI never uses
shell=Truewith subprocess. - Bandit security scan: The CI pipeline runs Bandit on every PR.
Can GRM be installed via pip?
Yes:
pip install grm
This installs the grm CLI and its Python dependencies. The Ansible playbooks and role are bundled with the package. For development or access to Make targets, clone the repository instead.
How does GRM handle idempotence?
The Ansible role is idempotent — running grm install twice produces zero changes on the second run. Each task checks for existing state before making changes. For example:
- User creation uses
ansible.builtin.userwhich only creates if the user doesn't exist - Package installation uses
state: presentwhich only installs if not already installed - Template creation uses
ansible.builtin.templatewhich only writes if the content changed - Rootless Docker setup uses
creates:to skip if already configured
This makes GRM safe for CI/CD pipelines and configuration management.