Manage runners with the CLI
List, create, update, pause, resume, duplicate and delete runners, wait for them, and read their metrics from the terminal.
The examples use the active profile and organization. Add --profile or --org to choose others, and --output json for scripts. Commands that change runners need the scopes from Authentication. runner update and --wait also need runners:read, which the CLI’s login token always has.
List and inspect
manage-runners runner list
manage-runners runner get --id 812999368881481087
manage-runners runner get --name build-01
Runner IDs are long numbers; treat them as strings. runner get --name needs an exact, unique name. The state field uses the API names: CREATING, CONFIGURING, READY (shown as Active in the dashboard), PAUSED, UNKNOWN and INVALID_GITLAB_TOKEN. See Runner states.
Find a server type and location
manage-runners product list
manage-runners product get --id hz-cpx31
manage-runners product locations --id hz-cpx31
product locations shows where Hetzner offers the server type and Hetzner’s monthly net price in each location. Use the location code, for example fsn1, with runner create.
To see the SSH keys of your Hetzner project, pass the Hetzner token on standard input or through an environment variable:
manage-runners provider hetzner ssh-keys --token-env HCLOUD_TOKEN
For an existing runner, runner ssh-keys --id <id> lists the keys available with the runner’s stored Hetzner token.
Create a runner
The CLI reads the GitLab runner token and the Hetzner token from environment variables you name, so they never appear on the command line:
export GLRT_TOKEN HCLOUD_TOKEN # set them from your secret manager
manage-runners runner create \
--name build-01 \
--product-id hz-cpx31 \
--location fsn1 \
--executor docker \
--concurrency 8 \
--gitlab-host https://gitlab.com/ \
--runner-token-env GLRT_TOKEN \
--hetzner-token-env HCLOUD_TOKEN \
--ssh-key-id 1234567 \
--label role=ci-runner \
--monitoring on \
--wait
| Flag | Required | Description |
|---|---|---|
--name |
yes | Letters, numbers, spaces and dashes |
--product-id |
yes | From product list |
--location |
yes | From product locations |
--executor |
yes | docker, dind or shell |
--concurrency |
yes | At least 1 |
--gitlab-host |
yes | Base URL of your GitLab instance |
--runner-token-env |
yes | Name of the variable holding the glrt- token |
--hetzner-token-env |
yes | Name of the variable holding the Hetzner token |
--ssh-key-id |
no | Hetzner SSH key ID, repeatable |
--label |
no | Hetzner label, repeatable |
--monitoring on|off |
no | Needs an active plan to turn on |
--wait |
no | Wait until the runner reaches a final state |
--yes |
no | Confirm creating paid resources without a prompt |
runner create creates a paid Hetzner server, so it asks create runner build-01 and potentially create paid resources? [y/N]. Pass --yes in scripts. Without a terminal and without --yes, it fails.
Each create request carries an idempotency key. The CLI generates a new one per invocation; pass your own with --idempotency-key (16 to 128 letters, digits, ., _ or -) to make a script’s retry safe. For the same user, organization, route, key and request, the API returns the first result until 24 hours after the first attempt; after that, the same request creates another runner. A different request with the same key, or a retry while the first attempt has not completed, is rejected as a conflict. Check runner list before retrying an old create. See Idempotency and unknown outcomes.
Wait for a runner
--wait on create, update, pause, resume and duplicate waits until the runner reaches READY, PAUSED, UNKNOWN or INVALID_GITLAB_TOKEN, for at most --timeout (default 10 minutes). Check the returned state: reaching UNKNOWN or INVALID_GITLAB_TOKEN ends the wait too.
To wait later or for a specific state:
manage-runners runner wait --id 812999368881481087 --state READY --timeout 15m
manage-runners runner watch --id 812999368881481087
If the wait times out, the command exits with code 7 (TIMEOUT). If it fails for another reason, for example a token without runners:read, it exits with that error. In both cases the change itself was accepted and is not undone; check the runner with runner get.
Update a runner
manage-runners runner update --id 812999368881481087 --name build-02 --label role=ci-runner
manage-runners runner update --id 812999368881481087 --concurrency 4 --yes --wait
Flags you leave out keep their current values. --label and --ssh-key-id replace the whole list.
--product-id,--gitlab-host,--runner-token-env,--concurrency,--executor,--ssh-key-idand--monitoringrecreate an active runner’s server and interrupt its jobs. The CLI asks for confirmation, or needs--yes.--name,--labeland--hetzner-token-envalone never recreate the server. See Changes that recreate the server.
The location cannot be changed.
Pause, resume and duplicate
manage-runners runner pause --id 812999368881481087 --yes
manage-runners runner resume --id 812999368881481087 --yes --wait
manage-runners runner duplicate --id 812999368881481087 --yes
pause deletes the server and keeps the IP addresses. resume and duplicate create servers and may cost money. All three ask for confirmation unless you pass --yes.
Delete a runner
Deleting needs the runners:delete scope and the runner ID twice, as --id and --confirm:
manage-runners runner delete --id 812999368881481087 --confirm 812999368881481087 --yes
Interactively, the CLI asks you to confirm and to type the ID. Delete removes the server, the IP addresses and the runner’s schedule. It does not remove the runner from GitLab.
If deletion cannot be confirmed with Hetzner, the runner is kept and the command fails. Run it again later.
Forget an Unknown runner
runner forget only works on a runner in the UNKNOWN state. It removes the record from Manage Runners and leaves any server and IP addresses in Hetzner untouched. It needs the runners:forget scope and the same confirmation as delete:
manage-runners runner forget --id 812999368881481087 --confirm 812999368881481087 --yes
Never use forget instead of delete. Try recovering the runner first, and clean up Hetzner yourself afterwards.
Read monitoring data
manage-runners runner metrics
manage-runners runner metrics --name build-01 --range 7d --output json
Without --id or --name, runner metrics covers every active runner with monitoring on. --range accepts 1h, 24h (default), 7d, 30d and 90d. Human output shows the latest CPU, RAM and disk usage and a summary of the history; JSON includes the full history.
latestis empty until the first sample arrives, andstalemarks samples older than three minutes.- A runner that is not monitored or not active fails with
NOT_MONITORED(exit code 5). - Without an active plan, the command fails with
SUBSCRIPTION_REQUIRED(exit code 4). - If the history is shorter than the range, a
RANGE_EXCEEDS_HISTORYwarning lists the available ranges.
If a change has an unknown outcome
If the connection fails while a change is in progress, the CLI exits with code 10 (UNKNOWN_OUTCOME). The change may or may not have happened. Check the runner with runner list or runner get before you try again; never retry blindly.
