Named Runners

Named runners let you deploy isolated runner containers, each with its own config.toml. Jobs are dispatched to a named runner when any of their CI tags match the runner name.

Quick start#

Define a named runner in your config and tag jobs to use it:

# ~/.glci/config.toml

[runners.gpu]
config_template = """
[[runners]]
  [runners.docker]
    gpus = "all"
    memory = "16g"
"""

This one lives in the global config because gpus is a privilege grant, which a project template may not set — see Config templates below. A project .glciconfig.toml can define named runners too, as long as its template stays inside the allowed keys (memory alone would be fine here).

# .gitlab-ci.yml

train-model:
  tags: [gpu]
  image: nvidia/cuda:12.0-devel
  script:
    - python train.py

When you run glci run, the train-model job is routed to the gpu runner container. All other jobs run on the default shared runner.

How it works#

The daemon’s RunnerManager coordinates three tiers of runner containers:

TierContainer nameWhen created
Sharedglci-runner-{suffix}Always (default for all projects)
Per-projectglci-runner-{suffix}-{project}When [runner] config_template is set (in either config file)
Namedglci-runner-{suffix}-{project}-{name}When [runners.<name>] is defined

Job dispatch#

  1. For each job, glci checks if any of its tags match a named runner name
  2. First tag match wins — the job is assigned to that named runner
  3. Unmatched jobs go to the default runner (shared or per-project)
  4. Named runners have run_untagged=false — they only receive explicitly tagged jobs

Config templates#

Each named runner accepts a config_template (inline Go template) or config_template_file (path to a template file) that generates its config.toml.

Project templates are restricted: a template in a project .glciconfig.toml may not set endpoint, privilege, or trust keys ([runners.docker] host/volumes/privileged/gpus/tmpfs/dns/…, executor, clone_url, environment, pre_build_script, [runners.kubernetes], top-level listen_address, …) — a project file travels with the repository, so a clone must not be able to choose where or how your jobs run. The pipeline fails naming the config file, section, key, and the offending setting. Templates in ~/.glci/config.toml are unrestricted, and a global template for a runner always wins over a project one. See Restricted keys in a project config template.

Inline template#

# ~/.glci/config.toml

[runners.gpu]
config_template = """
[[runners]]
  [runners.docker]
    gpus = "all"
    privileged = true
    volumes = ["/cache", "/usr/local/nvidia:/usr/local/nvidia:ro"]
"""

External template file#

[runners.gpu]
config_template_file = "runners/gpu-config.toml.tpl"

The template file uses the same Go text/template syntax. See Runner config templates for the full list of template variables.

Default runner templates#

The default runner ([runner]) can also use config_template to customize its config.toml. This is useful when you need to change runner-level settings globally without defining named runners:

# ~/.glci/config.toml

[runner]
config_template = """
[[runners]]
  [runners.docker]
    privileged = true
    network_mode = "host"
    shm_size = 2147483648
"""

When the default runner has a config_template, it gets its own per-project container (separate from the shared runner).

Multiple named runners#

You can define as many named runners as needed:

# ~/.glci/config.toml

[runners.gpu]
config_template = """
[[runners]]
  [runners.docker]
    gpus = "all"
    privileged = true
"""

[runners.arm]
config_template = """
[[runners]]
  [runners.docker]
    platform = "linux/arm64"
"""

[runners.shell]
config_template = """
[[runners]]
  executor = "shell"
"""
# .gitlab-ci.yml

train:
  tags: [gpu]
  script: python train.py

build-arm:
  tags: [arm]
  script: make build

deploy:
  tags: [shell]
  script: ./deploy.sh

lint:
  # no tags — runs on the default runner
  script: make lint

Runner name rules#

Runner names must match [a-zA-Z0-9][a-zA-Z0-9_-]*:

Docker host per runner#

Named runners can target different Docker daemons via docker_host (raw endpoint) or docker_context (Docker context name). The runner container and all job containers it spawns are created on the specified daemon.

Global config only: engine, docker_host, docker_context, container_socket, tls_cert_path, and tls_verify are honored only in ~/.glci/config.toml. A project .glciconfig.toml that sets them has them dropped, and glci prints a warning naming each ignored key — a project file travels with the repository, so honoring it would let a cloned repo point glci at a container daemon it chose. config_template/config_template_file still work per-project for runners the global config does not already template, minus the restricted keys that would reach the same settings. See Container-endpoint keys.

# ~/.glci/config.toml

# Using docker_host (raw endpoint URL)
[runners.colima]
docker_host = "unix:///Users/me/.colima/default/docker.sock"

# Using docker_context (Docker context name — respects TLS config)
[runners.arch]
docker_context = "arch"

[runners.gpu]
docker_host = "ssh://gpu-host"
config_template = '''
[[runners]]
  [runners.docker]
    gpus = "all"
'''

# TCP with TLS client certificates (Docker only)
[runners.secure]
docker_host = "tcp://build-host:2376"
tls_cert_path = "/path/to/certs"   # directory with ca.pem, cert.pem, key.pem
tls_verify = true                   # sets DOCKER_TLS_VERIFY=1

docker_host supports schemes: unix://, tcp://, ssh://, fd://.

For tcp:// endpoints that require TLS, set tls_cert_path to the directory containing ca.pem, cert.pem, and key.pem, and tls_verify = true to enable verification. Each runner can use different certificates, allowing connections to multiple TLS-secured Docker daemons.

Podman: tls_cert_path/tls_verify map to DOCKER_CERT_PATH/DOCKER_TLS_VERIFY, which only the Docker client honors. On a runner with engine = "podman", a tcp:// docker_host is not TLS-protected even with both keys set — glci config --network emits a warning. Instead, use an ssh:// endpoint — Podman’s only authenticated and encrypted remote transport. A podman system connection referenced with docker_context is only as secure as its own URI: a connection pointing at tcp:// is still unencrypted and unauthenticated.

docker_context takes a Docker context name (as shown by docker context ls), or a Podman connection name (podman system connection list) when the runner sets engine = "podman". This is simpler when the endpoint is already configured in the engine and respects its TLS settings.

If both are set, docker_host takes precedence.

Runner defined in both config files#

When the same runner name appears in ~/.glci/config.toml and in a project .glciconfig.toml, every field the global entry owns is carried over: the endpoint fields (engine, docker_host, docker_context, container_socket, tls_cert_path, tls_verify) and the config template. A project config therefore cannot set those endpoint fields or erase them: a bare [runners.gpu] in a project file does not clear a global docker_host/tls_verify and quietly move gpu-tagged jobs onto the local engine. Nothing is printed in that case — there is no ignored key to name, the global values simply still apply.

config_template / config_template_file follow the same one-way rule:

Global entryProject entryResult
no templatetemplateProject template applies, subject to the restricted keys
templateno templateGlobal template still applies
templatetemplateGlobal template wins; the project’s is dropped and reported as [runners.<name>] config_template (or config_template_file) in the ignored-keys warning

glci cannot tell a benign global template from one that carries [runners.docker] host/tls_cert_path/volumes, so the global one always wins — otherwise a project entry could redirect the runner by replacing the template instead of by naming an endpoint key.

The same applies to the default runner’s [runner] config_template.

Windows Docker daemons#

Named runners can target Windows Docker daemons. glci auto-detects whether the remote daemon runs Windows by probing docker info --format '{{.OSType}}' and uses the correct named pipe (//./pipe/docker_engine) for Docker socket mounts in runner containers.

For manual override, set container_socket:

# ~/.glci/config.toml

[runners.windows]
docker_host = "tcp://windows-host:2375"
container_socket = "//./pipe/docker_engine"

This is useful as a fallback when auto-detection is unavailable (e.g. restricted docker info access).

When a Windows daemon is on a different host (requiring a relay proxy), glci automatically uses the Windows variant of the glci image. It resolves registry-first on the remote daemon, taking the first hit: registry.gitlab.com/gitlab-org/ci-cd/runner-tools/glci:<commit>-windows (pulled if absent), then :latest-windows (pulled if absent), then a local glci:local-windows.

glci:local-windows comes last on purpose, unlike the Linux image where the local tag is preferred. make docker-windows builds with --load, which only populates your own Docker daemon — the remote Windows host is a different machine, so that tag is normally absent there and probing it first would just cost a remote round trip that misses. It is still honoured if you put it there yourself:

make docker-windows
docker save glci:local-windows | docker --context <remote> load

Remote runner behavior#

When a runner targets a remote Docker daemon, glci automatically:

graph LR subgraph your_machine["Your machine"] D["glci daemon"] M["glci-mock
(mock server)"] end subgraph remote_host["Remote Docker host"] R["Runner
(gitlab-runner)"] P["Relay proxy
(glci-proxy-*)"] J["Job containers"] end D -.->|"docker exec -i
(muxproto)"| M D -.->|"docker exec -i
(muxproto)"| P R -->|"HTTP via Docker DNS
(glci-mock:39741)"| P R -->|"Docker executor"| J J -->|"HTTP via extra_hosts
(glci-mock)"| P P -.->|"relay → daemon → mock"| D style your_machine fill:#ecfdf5,stroke:#10b981,stroke-width:2px style remote_host fill:#fef3c7,stroke:#f59e0b,stroke-width:2px

Jobs tagged to a remote runner execute entirely on that daemon — the runner container, job containers, and service sidecars all live on the remote host. Untagged jobs continue to run on the default (local) Docker daemon. Artifacts, cache, and git data all flow through the relay proxy transparently.

See also Docker & Network for the full network topology diagram and How It Works for the relay proxy architecture.

Lifecycle#

Esc