Configuration Files
File locations#
~/.glci/config.toml— user-wide defaults (concurrency, 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]
concurrency = 4 # max parallel jobs (0 = unlimited)
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
[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] | concurrency, 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 | 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) | 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 child 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"
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"
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"
Overrides are applied after CI YAML parsing and before runner execution. 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.