Configuration Files

File locations#

If no .glciconfig.toml exists when you run glci, one is created automatically with a default that skips the .pre stage. Set GLCI_NO_DEFAULT_CONFIG=1 to disable this behavior.

Config merging#

When both files exist, glci uses the global file as the base and loads the project file on top. The merged project config is used for [cache], [docker], [gitlab], [network], [paths], [tls], [runner], [pipelines], and [jobs] overrides, while [skip] and [manual] rules are combined additively. Per-job overrides ([jobs."<name>"]) are deep-merged: project values take precedence over global ones for the same job.

[token] is the exception: it is merged one-way, so a project config can only narrow host-token forwarding, never widen it — see Token forwarding in a project config. (A selected pipeline preset’s [token] replaces the top-level one outright when it sets forward_host_token; within a preset extends chain, [token] sections merge additively, since both layers are your own.) A further set of keys is honored only in the global file — see Container-endpoint keys directly below.

Container-endpoint keys#

A handful of keys decide which container endpoint glci talks to, how far it trusts that endpoint, and how much of the host it exposes to containers. Those keys are honored only in ~/.glci/config.toml:

SectionGlobal-only keys
[docker]engine, host, container_socket, privileged
[paths]container_builds_dir, container_cache_dir, container_certs_dir
[runners.<name>]engine, docker_host, docker_context, container_socket, tls_cert_path, tls_verify
[token]forward_host_token, jobs, job_patterns, stagesnarrow-only, see Token forwarding in a project config
[pipelines.<name>.token]the whole section, when it would turn forwarding back on
[runner], [runners.<name>]config_template, config_template_fileonly when the global config already templates that runner

A .glciconfig.toml travels with the repository, so it is untrusted input:

The keys are dropped, not the runner. A project .glciconfig.toml may still declare a [runners.<name>] the global config does not define, and it becomes a real named runner with its own container — what it cannot do is choose where that runner runs. With engine, docker_host, docker_context, container_socket and the TLS keys stripped, every runner a project introduces lands on the same engine and endpoint as the daemon itself, and the only thing it can still contribute is a config_template (checked against the deny-list). For a name the global config also defines, those fields are additionally re-taken from the global entry, so a project cannot override them there either.

If a project config sets any of them they are dropped before the merge and glci prints a warning on stderr naming each ignored key, for example:

glci: warning: .glciconfig.toml: ignoring [docker] host, [docker] privileged, [runners.gpu] docker_host — these settings are only honored in ~/.glci/config.toml, because a project file travels with the repository

The same line is written to ~/.glci/daemon.log, since the daemon is the process that would have driven the endpoint, and glci doctor shows it as a ~ Project config row. Everything else in a project config — skip/manual rules, pipeline presets, per-job overrides, [gitlab], [network], [cache], [tls], and the remaining [paths] keys — is unaffected.

For a named runner defined in both files, the global entry’s endpoint fields also survive the merge unchanged — a project entry can neither override them nor erase them by redefining the runner with those keys absent. The global entry’s config_template / config_template_file survives too; a project template is only used for a runner the global config does not already template. See Runner defined in both config files.

Container mount paths are validated#

Independently of where the value came from, the three container-side mount paths must be plain container paths. A value carrying a colon is not a mount point but a host:container bind, so glci rejects it before any container exists:

paths.container_cache_dir: "/:/hostroot" is not a container path — it must be an absolute path such as "/cache"; a "host:container" value would bind-mount a host directory into every job container

container_certs_dir additionally accepts the documented Docker named-volume form (certs-vol:/certs), since a volume name is not a host path:

paths.container_certs_dir: "/host/certs:/certs" is not a container path — it must be an absolute path such as "/certs", or a Docker volume name such as "certs-vol:/certs"; a "host:container" value would bind-mount a host directory into every job container

Windows container paths (C:\builds) are accepted as-is.

Token forwarding in a project config#

A project .glciconfig.toml may only narrow host-token forwarding, never widen it:

Project [token]Global [token]Result
forward_host_token = falseanythingHonored — forwarding is off
forward_host_token = trueforward_host_token = falseIgnored and reported as [token] forward_host_token
forward_host_token = trueunset or trueNo change (forwarding is already on by default)
jobs / job_patterns / stagesno filters setHonored — the project narrows the “every job” default
jobs / job_patterns / stagesany filter setIgnored and reported per key ([token] jobs, …)

Two sets of regexes cannot be intersected, so when you have already restricted forwarding your own filters stand and the project’s are dropped.

The same rule covers a project pipeline preset: a selected preset’s [token] section replaces the top-level one, so [pipelines.<name>.token] forward_host_token = true in a project file would otherwise route around the table above. When your global [token] restricts forwarding at all (an explicit forward_host_token = false, or any job/stage filter), a project preset that turns it back on has its whole [token] section ignored and reported as [pipelines.<name>.token]. Turning forwarding off in a preset is a narrowing and stays honored.

The default — no [token] section anywhere, meaning forwarding is on for every job — is glci’s own default and is untouched.

Restricted keys in a project config template#

A config_template can reach the same settings from the other direction, since it is rendered straight into gitlab-runner’s config.toml. A template supplied by a project config is therefore checked against a deny-list after rendering — by either key, config_template or config_template_file, since a path in the project file loads project-controlled content just the same. Lookups are case-insensitive (gitlab-runner decodes its config.toml that way, so [runners.DOCKER] HOST counts), and the check inspects the rendered TOML, not the template text. It may not set:

LocationDenied keys
[[runners]]url, token, executor, clone_url, environment, pre_build_script, post_build_script, pre_clone_script, pre_get_sources_script, post_get_sources_script, step_runner_image
[[runners]] tables[runners.kubernetes], [runners.ssh], [runners.custom], [runners.machine], [runners.instance], [runners.parallels], [runners.virtualbox], [runners.autoscaler], [runners.cache]
[runners.docker] — endpoint & trusthost, tls_cert_path, tls_verify, cert_path
[runners.docker] — host filesystem & devicesvolumes, volumes_from, volume_driver, volume_driver_ops, devices, services_devices, device_cgroup_rules, tmpfs, services_tmpfs
[runners.docker] — privilege & capabilitiesprivileged, services_privileged, cap_add, cap_drop, security_opt, services_security_opt, sysctls, runtime, userns_mode, group_add, gpus, service_gpus
[runners.docker] — namespace sharingnetwork_mode, ipcmode, pid_mode, pid, links, hostname
[runners.docker] — name resolutionextra_hosts, dns, dns_search
[runners.docker] — image trust & allow-listshelper_image, helper_image_flavor, allowed_images, allowed_services, allowed_users, allowed_pull_policies, allowed_privileged_images, allowed_privileged_services, services_limit
top level[session_server], listen_address

cert_path and pid are not gitlab-runner keys — they are kept in the list as guards, so a rename or a typo can never become an accepted key. The real names are tls_cert_path and pid_mode.

Everything else still works: a project template may set concurrent, [runners.docker] image / pull_policy / memory / shm_size, [runners.feature_flags], and so on. The pipeline fails naming the config file, section, key, and the offending setting:

daemon: /path/to/repo/.glciconfig.toml [runners.gpu] config_template sets [runners.docker] host, which a project config may not change because it controls where and how jobs run; move this setting to ~/.glci/config.toml

For the default runner the section reads [runner] config_template (or config_template_file, whichever you wrote) — glci never names a [runners.default] section, because that is its internal runner identity and not something you can edit.

Templates in ~/.glci/config.toml — including [runners.<name>] config_template / config_template_file there — are your own and are never restricted. A project template is also only used at all where the global config supplies none for that runner: if both files template the same runner, the global template wins and the project’s config_template / config_template_file is reported as an ignored key.

[gitlab] url in a project config#

A project config may set [gitlab] url — per-project self-hosted instances are a supported setup. But because the file travels with the repository, a URL it supplies is never paired with a credential the repository did not itself name. When [gitlab] url comes from .glciconfig.toml, glci sends a token only if that token came from the same project config file ([gitlab] token next to the URL) or from an explicit --token. Every other source is withheld:

Withheld sourceNamed in the warning as
[gitlab] token in ~/.glci/config.tomlglobal config file (~/.glci/config.toml)
GITLAB_TOKEN / GITLAB_PRIVATE_TOKENenvironment (GITLAB_TOKEN / GITLAB_PRIVATE_TOKEN)
glab auth credential-helperglab credential-helper
glab CLI config (legacy fallback)glab CLI config

The global config’s token is included because it is your credential for your own instance: sending it to a host the repository named leaks it exactly as an ambient token would.

With no --token and no [gitlab] token in the project file, GitLab API requests to that URL go out unauthenticated — the same behavior as having no token configured, so remote include: and project: includes are skipped and parsing stays offline. glci says so once on stderr, naming the withheld source (never the token value):

glci: warning: [gitlab] url "http://attacker.example" comes from the project config, which travels with the repository, so the token from your environment (GITLAB_TOKEN / GITLAB_PRIVATE_TOKEN) was withheld — GitLab API requests to it are unauthenticated. Set [gitlab] token in that project config or pass --token if you trust this instance.

glci config --gitlab reports the same state as token = <withheld>, and glci doctor flags it. To authenticate against a project-supplied instance, opt in explicitly with [gitlab] token in that project config or --token. A [gitlab] url set in ~/.glci/config.toml, given with --gitlab-url, or read from GITLAB_URL is your own and uses every token source as before.

Project URL paired with an env-ref token#

Pairing a project URL with an env-ref token — token = "$SOME_VAR" in the same .glciconfig.toml — is allowed and keeps working: env refs are the documented way to avoid committing a secret, and a team’s own checked-in config legitimately does exactly this. But the repository is then choosing both the host variable to read and where its value goes, so glci warns once on stderr, naming the variable (never its value):

glci: warning: the project config sends $SOME_VAR to [gitlab] url "https://gitlab.example.com" — both the variable and the URL come from this repository, so check that you trust it with that value.

Nothing is blocked and the request is authenticated as usual. If the pairing is your own team’s config, the warning is just confirmation; if it is a repository you cloned, check the URL before trusting it with that variable.

Inspecting config#

glci config                    # show effective merged configuration
glci config --gitlab           # show resolved GitLab instance config
glci config --network          # show network, paths, and TLS config
glci config init               # scaffold a starter .glciconfig.toml
glci config edit               # open project config in $EDITOR

Project config example#

# .glciconfig.toml

[skip]
stages = ["deploy"]

[manual]
jobs = ["deploy-production"]

[gitlab]
url = "https://gitlab.example.com"
token = "$GITLAB_TOKEN"

# Per-job field overrides (override CI YAML fields without touching .gitlab-ci.yml)
[jobs."build".image.docker]
platform = "linux/amd64"

[jobs."build".variables]
CC = "x86_64-linux-gnu-gcc"

Because [gitlab] url here comes from the project file, keep [gitlab] token in that same file — no other token source is sent to a project-supplied URL. glci prints one warning naming $GITLAB_TOKEN, since the repository picked both the variable and the destination — see Project URL paired with an env-ref token. Move the pair to ~/.glci/config.toml if you would rather not see it.

Full global config example#

# ~/.glci/config.toml

[defaults]
executor = "docker"
dirty = false            # disable dirty mode (default: true)
image = "alpine:latest"

# [cache]
# persistent = false                   # disable cross-pipeline CI cache
# max_size = "100MB"                   # reject uploads when total exceeds this
# ttl = "24h"                          # expire entries older than this

# [docker]
# engine = "docker"                     # container engine: "docker" or "podman"
#                                       # (left unset: prefer Docker, auto-fall back to Podman;
#                                       #  uncommenting opts out of that fallback)
# host = "ssh://remote-host"
# container_socket = "/var/run/docker.sock"
# privileged = false                    # runner container privilege — global-only

[registry]
# url = "registry.gitlab.com"
# username = "my-user"
# password = "$REGISTRY_PASSWORD"

# [runner]
# default_version = "17.6.0"
# image = "registry.gitlab.com/.../gitlab-runner:tag"  # full image override
# args = ["--debug"]
# config_template = """..."""    # inline Go template for runner config.toml
# config_template_file = "runner-config.toml.tpl"  # path to a Go template file

# [runners.gpu]                  # named runner — gets its own container
# config_template = """..."""    # inline Go template for this runner
# docker_host = "ssh://gpu-host" # run on a different Docker daemon (endpoint URL)
# docker_context = "colima"      # or use a Docker context name instead
# engine = "podman"              # container engine for this runner (needs a distinct docker_host/docker_context)
# container_socket = "//./pipe/docker_engine"  # override Docker socket path (auto-detected; use for Windows daemons)

[daemon]
idle_timeout = "30m"     # auto-stop daemon after idle
# max_log_size = "50MB"  # rotate daemon.log when it exceeds this size (set to "0" to disable)
# max_log_files = 3      # number of rotated log files to keep (0 = no backups, log is truncated)

# Context simulation presets
[contexts.staging]
context = "branch=staging"
env = { DEPLOY_ENV = "staging" }

[contexts.release]
context = "tag=v1.0"
env = { RELEASE = "true" }

Full config schema#

SectionKeysScope
[defaults]executor, image, dirtyGlobal
[docker]engine, host, container_socket, privilegedGlobal — see Container-endpoint keys
[gitlab]url, token, secrets_ttl, runner_releases_urlBoth — a project-set url only gets a token from that same project file or --token, see [gitlab] url in a project config
[registry]url, username, password, push_throughGlobal
[registry.upstream]username, passwordGlobal
[cache]persistent, max_size, ttlBoth
[daemon]idle_timeout, socket, log_file, max_log_size, max_log_filesGlobal
[network]mock_server_port, mock_server_bind, container_host, host_gateway, registry_bind, registry_http_bind, daemon_socketBoth
[network.extra_hosts]entriesBoth
[paths]container_ca_cert, container_ca_bundle, container_docker_certs, home, registry_storage, registry_ca_dirBoth
[paths]container_builds_dir, container_cache_dir, container_certs_dirGlobal — see Container-endpoint keys
[tls]extra_sans, cert_validityBoth
[runner]default_version, image, argsBoth
[runner]config_template, config_template_fileBoth — a project-supplied template is checked against a deny-list, and is ignored when the global config templates the same runner, see Restricted keys in a project config template
[runners.<name>]config_template, config_template_fileBoth — same deny-list and same global-wins rule as [runner]
[runners.<name>]engine, docker_host, docker_context, container_socket, tls_cert_path, tls_verifyGlobal — see Container-endpoint keys
[contexts.<name>]context, mr_source, mr_target, envGlobal
[skip]jobs, job_patterns, stagesBoth (additive)
[manual]auto_run, jobs, job_patternsBoth (additive)
[token]forward_host_token, jobs, job_patterns, stagesBoth — a project config may only narrow, see Token forwarding in a project config
[pipelines.<name>]See Pipeline PresetsBoth — a project preset’s [token] section is ignored when it would widen the global one
[projects."<path>"]dir, branchBoth
[jobs."<name>"]Per-job field overrides (any CI YAML field); name may be a glob patternBoth

Common configurations#

Copy-pasteable TOML snippets for frequently needed setups.

Minimal project config#

The most common starting point. Skip stages that cannot run locally and auto-run specific manual jobs:

# .glciconfig.toml

[skip]
stages = [".pre", "deploy"]

[manual]
jobs = ["build-docker-image"]

Self-managed GitLab instance#

Connect to a self-managed GitLab instance. The token uses an env var reference to avoid committing secrets:

# .glciconfig.toml

[gitlab]
url = "https://gitlab.example.com"
token = "$GITLAB_EXAMPLE_TOKEN"

Keep both keys in the same file: a project-supplied URL only ever gets a token from that project config or --token. glci prints one warning naming $GITLAB_EXAMPLE_TOKEN, because the repository picked both the variable and the destination — see Project URL paired with an env-ref token. Put the pair in ~/.glci/config.toml instead if you would rather not see it.

Remote Docker host#

Run containers on a remote machine (e.g. a powerful build server) instead of the local Docker daemon. SSH-based connections work out of the box if you have SSH key access:

# ~/.glci/config.toml

[docker]
host = "ssh://build-server"

For TCP connections (e.g. Docker exposed on a LAN host):

# ~/.glci/config.toml

[docker]
host = "tcp://192.168.1.100:2375"
container_socket = "/var/run/docker.sock"

[docker] host is the highest-priority setting, but it is not the only way in: an exported DOCKER_HOST (or CONTAINER_HOST under Podman) selects a remote daemon for a single shell with no config changes, outranking the active Docker context. See Environment variables.

Container engine (Docker or Podman)#

The [docker] engine key selects the container engine backing the default daemon: "docker" or "podman". This is orthogonal to [defaults] executor — the gitlab-runner executor stays "docker" even under Podman, because gitlab-runner has no Podman executor; glci simply points it at Podman’s Docker-compatible API socket. Do not set executor = "podman".

Auto-fallback applies only when engine is unset (the key is omitted entirely): glci tries Docker first and falls back to Podman if Docker is unreachable but Podman is. The fallback notice is printed to the terminal the first time the daemon starts and logged in the daemon log. Any explicit value is honored as-is, with no probing:

Podman requires a running API socket — start it with systemctl --user enable --now podman.socket (rootless), sudo systemctl enable --now podman.socket (rootful), or podman machine start (macOS/Windows). On macOS and Windows the socket lives inside the podman machine VM: glci picks up the machine’s ssh:// endpoint and its SSH identity automatically, and glci doctor reports the socket as “cannot verify from host” because nothing on the host can dial it. The same warning appears on Linux when the resolved Podman endpoint (CONTAINER_HOST, else the default podman system connection) is not a local unix:// path, since a remote socket is just as unreachable from here. See Docker & Network > Podman.

# ~/.glci/config.toml

[docker]
engine = "podman"

The engine is a global setting (~/.glci/config.toml) — see Container-endpoint keys. The daemon resolves its engine once at startup from the global config only, so:

To run a single named runner on a different engine, set engine on that runner in the global config together with a distinct docker_host/docker_context (see Named runners).

Optimizing for CI-heavy repos#

For projects with large pipelines, tune cache and runner settings:

# ~/.glci/config.toml

[defaults]
dirty = true          # include uncommitted files (default)

[cache]
persistent = true     # keep CI cache across runs (default)
max_size = "1GB"      # allow larger cache
ttl = "72h"           # keep cache entries for 3 days

Pipeline presets for different workflows#

Define named presets that bundle skip rules, context, and variables. Run them with glci run -p <name>:

# .glciconfig.toml

[pipelines.quick]
stages = ["lint", "test"]
env = { CI_QUICK = "1" }

[pipelines.quick.skip]
job_patterns = [".*windows.*", ".*macos.*"]

[pipelines.deploy-staging]
context = "branch=staging"
env = { DEPLOY_ENV = "staging" }

[pipelines.deploy-staging.manual]
auto_run = true    # auto-run all manual jobs in this preset

[pipelines.mr]
context = "merge_request"
mr_source = "feature-branch"
mr_target = "main"
glci run -p quick
glci run -p deploy-staging
glci run -p mr

Multi-project workspace#

When your pipeline uses trigger: project: to spawn cross-project pipelines in other repos, map those projects to local directories:

# .glciconfig.toml

[projects."mygroup/shared-library"]
dir = "../shared-library"
branch = "main"

[projects."mygroup/deploy-tools"]
dir = "../deploy-tools"

This lets trigger jobs clone from the local checkout instead of fetching from GitLab.

Token forwarding control#

By default, glci forwards your host GitLab token as CI_JOB_TOKEN so jobs can call the real GitLab API. Restrict this to specific jobs or stages:

# ~/.glci/config.toml

[token]
forward_host_token = true
stages = ["deploy", "release"]
jobs = ["publish-package"]
job_patterns = [".*-publish$"]

To disable token forwarding entirely (all jobs get a synthetic mock token):

[token]
forward_host_token = false

A project .glciconfig.toml may also set [token], but only to narrow what the global config allows — forward_host_token = false is always honored, while a project value that would hand the token to more jobs than you allowed is dropped and reported. See Token forwarding in a project config.

Registry with push-through mirroring#

Mirror local docker push operations to the upstream registry. Useful for building and publishing images locally:

# ~/.glci/config.toml

[registry]
push_through = true

[registry.upstream]
username = "deploy-token"
password = "$REGISTRY_WRITE_TOKEN"

Custom runner configuration#

Pin a specific gitlab-runner version, add debug logging, or use a custom runner image:

# ~/.glci/config.toml

[runner]
default_version = "17.8.0"
args = ["--debug"]

Runner config templates#

Use config_template (inline) or config_template_file (path to file) to provide a Go template that produces the runner’s config.toml. The template receives a RunnerTemplateContext with fields from the base runner configuration. config_template takes precedence over config_template_file.

If the rendered template produces multiple [[runners]] blocks, glci automatically derives sub-tokens and registers each runner separately with the mock server.

A template in a project .glciconfig.toml may not set endpoint, privilege, or trust keys — see restricted keys in a project config template. The example below uses the global config, where nothing is restricted.

# ~/.glci/config.toml

[runner]
config_template = """
[[runners]]
  executor = "{{.Executor}}"
  [runners.docker]
    image = "{{.DefaultImage}}"
    privileged = {{not .DisablePrivileged}}
    pull_policy = ["{{.PullPolicy}}"]
    volumes = ["{{.CacheDir}}", "{{.CertsVolume}}"{{range .ExtraVolumes}}, "{{.}}"{{end}}]
"""

Note: Fields name, url, and token in [[runners]] blocks are always overwritten by glci with auto-generated values. You do not need to set them in your template — use {{.URL}} only if other parts of your config reference the URL.

Or reference an external template file:

[runner]
config_template_file = "runner-config.toml.tpl"

Limiting job parallelism#

The number of jobs glci runs in parallel is governed by the gitlab-runner concurrent setting, which defaults to 10. To change it, set concurrent at the top level of a runner config template:

# .glciconfig.toml

[runner]
config_template = """
concurrent = 1

[[runners]]
"""

The empty [[runners]] block is required — a template with no runner entry is rejected. glci fills in the runner’s name, url, token, and the rest of the defaults; only the top-level concurrent value is overridden here.

concurrent is not on the deny-list, so this works from a project config — as long as ~/.glci/config.toml does not template the same runner, in which case the global template wins and this one is reported as an ignored key.

Template variables#

The template receives a context with these fields:

VariableTypeDescription
.NamestringRunner entry name (auto-generated)
.URLstringMock server URL
.Tags[]stringTags assigned to this runner
.ExecutorstringExecutor type (e.g. "docker")
.DefaultImagestringDefault Docker image
.PullPolicystringDocker pull policy ("always", "if-not-present", "never")
.HelperImagestringOverride for the helper image
.ExtraEnvironment[]stringAdditional KEY=VALUE env vars
.FeatureFlagsmap[string]boolFeature flags (e.g. FF_NETWORK_PER_BUILD)
.CacheServerAddressstringS3 cache server address
.CacheSecretKeystringS3 cache secret key
.CacheDirstringCache mount path (default "/cache")
.CertsVolumestringDinD TLS certs volume (default "/certs")
.ExtraVolumes[]stringAdditional volume mounts
.DisablePrivilegedboolWhen true, sets privileged = false
.ExtraHosts[]stringExtra host-to-IP mappings
.PreBuildScriptstringShell snippet injected before job scripts

Named runners#

Define named runners under [runners.<name>] to get isolated containers with independent config.toml files. Jobs whose tags include the runner name are dispatched to that runner instead of the default one.

# ~/.glci/config.toml

# A GPU runner with a custom config template. privileged/gpus/volumes make this a
# global-config template — see "Restricted keys in a project config template".
[runners.gpu]
config_template = """
[[runners]]
  [runners.docker]
    privileged = true
    gpus = "all"
    volumes = ["/cache", "/usr/local/nvidia:/usr/local/nvidia:ro"]
"""

# An ARM runner using an external template file
[runners.arm]
config_template_file = "runners/arm-config.toml.tpl"

Runners that target their own endpoint must be defined in the global config — those keys are ignored in a project file (see Container-endpoint keys):

# ~/.glci/config.toml

# A runner on a different Docker daemon (endpoint URL)
[runners.remote]
docker_host = "ssh://build-server"

# A runner using a Docker context name
[runners.colima]
docker_context = "colima"

# A runner backed by Podman on a separate endpoint
[runners.podman]
docker_host = "ssh://podman-host"   # distinct endpoint required (default daemon is process-global)
engine = "podman"

# A runner on a TLS-secured TCP endpoint (Docker only — see the note below)
[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                   # enables TLS verification

Note: tls_cert_path/tls_verify are implemented with DOCKER_CERT_PATH/DOCKER_TLS_VERIFY, which only the Docker client honors. Podman ignores them, so a tcp:// docker_host on a runner with engine = "podman" is not TLS-protected even with these keys set (glci config --network reports a warning). For a remote Podman endpoint, 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.

In your .gitlab-ci.yml, tag jobs to target a named runner:

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

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

Runner names must match [a-zA-Z0-9][a-zA-Z0-9_-]* (letters, digits, hyphens, underscores).

Isolation model#

Each named runner runs in its own Docker container with an independent config.toml:

Network customization#

Override bind addresses and container host resolution. Primarily needed for non-standard Docker setups or corporate networks:

# ~/.glci/config.toml

[network]
container_host = "host.docker.internal"   # default; how containers reach the host
host_gateway = "172.17.0.1"               # explicit gateway IP (default: auto-detect)

[network.extra_hosts]
entries = ["internal-registry.corp:10.0.0.50"]

[tls]
extra_sans = ["build-server.local"]       # additional SANs for generated TLS certs
cert_validity = "48h"                     # server cert lifetime (default "24h")

Per-job field overrides#

Override any CI YAML field for specific jobs without touching .gitlab-ci.yml. Useful for forcing a Docker platform, injecting variables, or changing the image locally:

# .glciconfig.toml

# Force linux/amd64 platform for a specific job
[jobs."build".image.docker]
platform = "linux/amd64"

# Inject extra variables into a job
[jobs."build".variables]
CC = "x86_64-linux-gnu-gcc"
GOARCH = "amd64"

# Override the image for a job
[jobs."test".image]
name = "ruby:3.2"

# Change job behavior
[jobs."deploy"]
when = "manual"
allow_failure = true

Flat dotted keys are also supported as equivalent sugar:

# These two forms are equivalent:

# Nested TOML tables
[jobs."build".image.docker]
platform = "linux/amd64"

# Flat dotted keys
[jobs."build"]
"image.docker.platform" = "linux/amd64"

Supported fields include: image, services, variables, script, before_script, after_script, stage, when, allow_failure, retry, timeout, tags, dependencies, needs, artifacts, cache, parallel, interruptible, hooks, and more.

Overrides apply in two phases, depending on the field:

Glob patterns#

The job-name key can be a glob pattern instead of an exact name, so you can override a field for many jobs (or all of them) at once. A key is treated as a pattern when it contains * or ?:

# .glciconfig.toml

# Strip the entrypoint for every job in the project
[jobs."*".image]
entrypoint = [""]

# Force a platform for all build-* jobs
[jobs."build-*".image.docker]
platform = "linux/amd64"

When several keys match the same job, their fields are deep-merged so overrides compose — a broad [jobs."*"] can set defaults while a more specific key adds or replaces individual fields. On a conflicting field, the most specific match wins:

# Every job gets KEY=base; the "build" job additionally gets KEY=override
# (exact match wins) plus EXTRA=1 from the glob.
[jobs."*".variables]
KEY = "base"
EXTRA = "1"

[jobs."build".variables]
KEY = "override"

A key is treated as a glob only when it contains * (matches any run of characters, including none) or ? (matches exactly one character). Every other character — including [, spaces, and / — is matched literally, so:

For non-structural fields, matching uses the job’s final (post-expansion) name, so both base names and generated shard names like test 1/3 or test: [linux, amd64] are targetable. The structural fields (stage, needs, parallel) are the exception: they are resolved before expansion and so match only the base job name — an override on the base job is inherited by every shard it expands into (so [jobs."*"] still reaches all shards), but you cannot set stage/needs/parallel on a single generated shard.

Brace alternation#

Keys also support shell-style brace alternation {a,b} (the same File::FNM_EXTGLOB semantics GitLab uses for exists:/changes:), so you can target a specific set of jobs in one key. Braces combine with the */? wildcards, and each expanded alternative is matched on its own:

# Applies to exactly the "build" and "test" jobs
[jobs."{build,test}".variables]
CGO_ENABLED = "0"

# Applies to build-amd, build-arm, test-amd, test-arm, …
[jobs."{build,test}-*".image.docker]
platform = "linux/amd64"

A wildcard-free alternative that equals the job name (e.g. build from {build,test}) counts as an exact match for precedence — it beats a broad * on conflicting fields.

Esc