Configuration Files
File locations#
~/.glci/config.toml— user-wide defaults (executor, daemon, registry, context presets, runner settings).glciconfig.toml— per-repo overrides, discovered by walking up from the current working directory
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], [manual], and [token] rules are combined additively. Per-job overrides ([jobs."<name>"]) are deep-merged: project values take precedence over global ones for the same job.
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"
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]
# host = "ssh://remote-host"
# container_socket = "/var/run/docker.sock"
# privileged = false
[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
# 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#
| Section | Keys | Scope |
|---|---|---|
[defaults] | executor, image, dirty | Global |
[docker] | host, container_socket, privileged | Both |
[gitlab] | url, token, secrets_ttl, runner_releases_url | Both |
[registry] | url, username, password, push_through | Global |
[registry.upstream] | username, password | Global |
[cache] | persistent, max_size, ttl | Both |
[daemon] | idle_timeout, socket, log_file, max_log_size, max_log_files | Global |
[network] | mock_server_port, mock_server_bind, container_host, host_gateway, registry_bind, registry_http_bind, daemon_socket | Both |
[network.extra_hosts] | entries | Both |
[paths] | container_builds_dir, container_cache_dir, container_certs_dir, container_ca_cert, container_ca_bundle, container_docker_certs, home, registry_storage, registry_ca_dir | Both |
[tls] | extra_sans, cert_validity | Both |
[runner] | default_version, image, args, config_template, config_template_file | Both |
[runners.<name>] | config_template, config_template_file, docker_host, docker_context, container_socket, tls_cert_path, tls_verify | Both |
[contexts.<name>] | context, mr_source, mr_target, env | Global |
[skip] | jobs, job_patterns, stages | Both (additive) |
[manual] | auto_run, jobs, job_patterns | Both (additive) |
[token] | forward_host_token, jobs, job_patterns, stages | Both |
[pipelines.<name>] | See Pipeline Presets | Both |
[projects."<path>"] | dir, branch | Both |
[jobs."<name>"] | Per-job field overrides (any CI YAML field); name may be a glob pattern | Both |
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"
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"
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:
# .glciconfig.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
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.
# .glciconfig.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, andtokenin[[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.
Template variables#
The template receives a context with these fields:
| Variable | Type | Description |
|---|---|---|
.Name | string | Runner entry name (auto-generated) |
.URL | string | Mock server URL |
.Tags | []string | Tags assigned to this runner |
.Executor | string | Executor type (e.g. "docker") |
.DefaultImage | string | Default Docker image |
.PullPolicy | string | Docker pull policy ("always", "if-not-present", "never") |
.HelperImage | string | Override for the helper image |
.ExtraEnvironment | []string | Additional KEY=VALUE env vars |
.FeatureFlags | map[string]bool | Feature flags (e.g. FF_NETWORK_PER_BUILD) |
.CacheServerAddress | string | S3 cache server address |
.CacheSecretKey | string | S3 cache secret key |
.CacheDir | string | Cache mount path (default "/cache") |
.CertsVolume | string | DinD TLS certs volume (default "/certs") |
.ExtraVolumes | []string | Additional volume mounts |
.DisablePrivileged | bool | When true, sets privileged = false |
.ExtraHosts | []string | Extra host-to-IP mappings |
.PreBuildScript | string | Shell 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.
# .glciconfig.toml
# A GPU runner with a custom 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"
# 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 on a TLS-secured TCP endpoint
[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
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:
- The default runner handles all untagged jobs and jobs not matching any named runner.
- Each named runner gets a dedicated container named
glci-runner-<suffix>-<project>-<runner-name>. - Containers are created lazily on first use and reused across pipeline runs.
- Tag matching uses first-match – if a job has multiple tags and more than one matches a named runner, the first matching tag wins.
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:
- Structural fields —
stage,needs, andparallel— are applied to the job definition immediately after CI YAML parsing, beforeparallel/matrixexpansion and before planning, so they take effect exactly as if written in.gitlab-ci.yml: overridingstagere-sequences the job, an overriddenneedsrewires the dependency graph, and settingparallelexpands the job into shards. Because they run before expansion, these keys match the base job name (the name written in.gitlab-ci.yml), never a generated shard name. An overriddenstagemust name a stage that already exists instages:, otherwise the job is dropped from the pipeline (glci cannot add a stage that the YAML did not declare). - All other fields (image, variables, script, tags, …) are applied when the job runs, matched against the job’s final name. For parallel/matrix jobs this is the expanded shard name, so a key can target a single shard (e.g.
[jobs."test: [linux, amd64]".image]).
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:
- an exact job-name key always beats any glob pattern, and
- among globs, the one with more literal (non-wildcard) characters wins — e.g.
build-*beats*.
# 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:
[jobs."*"]applies to every job in the pipeline, including parallel/matrix shards such astest 1/3andtest: [linux, amd64].- A key with
[but no*/?(e.g.test: [linux, amd64]) is matched exactly, never as a character class.
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.