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], [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#

SectionKeysScope
[defaults]concurrency, executor, image, dirtyGlobal
[docker]host, container_socket, privilegedBoth
[gitlab]url, token, secrets_ttl, runner_releases_urlBoth
[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_builds_dir, container_cache_dir, container_certs_dir, container_ca_cert, container_ca_bundle, container_docker_certs, home, registry_storage, registry_ca_dirBoth
[tls]extra_sans, cert_validityBoth
[runner]default_version, image, args, config_template, config_template_fileBoth
[runners.<name>]config_template, config_template_fileBoth
[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
[pipelines.<name>]See Pipeline PresetsBoth
[projects."<path>"]dir, branchBoth
[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, 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"

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.

# .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:

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.

Esc