Docker & Network
Network topology#
Three network layers exist at runtime:
| Network | Scope | Containers | Purpose |
|---|---|---|---|
| glci-net-{pipelineID} | Per pipeline | glci-mock, glci-runner, glci-runner-<project> (if any) | Runners poll mock for jobs via Docker DNS (glci-mock:39741) |
| Per-build network | Per job | Job container, service sidecars | Service DNS aliases (postgres, redis, etc.) |
| glci-net-{pipelineID}-job-{jobID} | Per DinD job | DinD job, docker:dind, glci-mock (attached) | Isolates DinD docker alias; gives DinD access to the embedded registry |
How each component reaches the mock server:
- Runners → Docker DNS on the pipeline network (
glci-mock:39741). The sharedglci-runnerholds N[[runners]]blocks from aconfig_template— each polls independently with its own HMAC-derived token. Per-project runners (glci-runner-<project>) are separate containers on the same network. - Job containers →
extra_hostsmappinghost.docker.internalto the Docker host gateway, then the published host port (default39741) - DinD daemons →
glci-mockis attached to the per-job DinD network, configured viainsecure-registriesindaemon.json - Daemon →
docker exec -iwith muxproto binary framing over stdin/stdout (works with local and remote Docker) - Remote runner jobs → Docker DNS on the remote bus network resolves
glci-mockto a relay proxy container, which relays requests back to the daemon viadocker execmuxproto (see Per-runner Docker host below)
Docker host configuration#
The endpoint can come from the config file or from the environment. [docker] host wins; an exported DOCKER_HOST (or CONTAINER_CONNECTION/CONTAINER_HOST under Podman) outranks the active docker context, matching the engines’ own precedence:
DOCKER_HOST=tcp://remote-host:2375 glci run # per shell, no config changes
The daemon resolves one endpoint at startup and is shared by every project, so switching later requires glci daemon stop. See Endpoint resolution order for the full table and Endpoint changes and the shared daemon for what glci reports when they disagree.
To set it persistently, configure the Docker daemon endpoint in ~/.glci/config.toml:
# ~/.glci/config.toml
[docker]
host = "tcp://remote-host:2375" # or "ssh://remote-host"
container_socket = "/var/run/docker.sock"
privileged = true # default; set false to disable --privileged
Global config only:
engine,host,container_socket, andprivilegedare honored only in~/.glci/config.toml. A project.glciconfig.tomlthat sets them has them dropped, with a warning naming each ignored key — a project file travels with the repository, so honoring it would let a cloned repo redirect glci to a container daemon it chose, or re-grant a privilege you turned off.privilegedis included because it applies to the runner container, the one glci mounts the engine socket into. See Config files > Container-endpoint keys.
Supported schemes for host:
unix:///var/run/docker.sock– local Docker socket (default)tcp://remote-host:2375– TCP connection to a remote Docker daemonssh://remote-host– SSH tunnel to a remote Docker daemonfd://– systemd socket activation (rare)
The container_socket setting controls the Docker socket path mounted inside runner containers. This is auto-detected by default. Manual configuration is only needed when the Docker daemon uses a non-standard socket path.
For named runners targeting Windows Docker daemons, glci auto-detects the daemon’s OS type via docker info and uses the correct socket path (//./pipe/docker_engine) automatically. If auto-detection fails (e.g. restricted docker info access), it falls back to /var/run/docker.sock (Linux default). Use container_socket on the named runner config to override. See Named Runners for details.
Set privileged = false in ~/.glci/config.toml to disable the --privileged flag on runner containers. This may be required on some security-hardened systems, but will break Docker-in-Docker pipelines. It is global-only: a project .glciconfig.toml cannot set it, and therefore cannot turn your privileged = false back on.
Podman#
Set engine = "podman" in [docker] to back the default daemon with Podman instead of Docker. This is orthogonal to [defaults] executor, which stays "docker" — gitlab-runner has no Podman executor, so glci points it at Podman’s Docker-compatible API socket (mounted into the runner container at the usual /var/run/docker.sock). Auto-fallback happens only when engine is unset (omitted): glci tries Docker first and falls back to Podman if only Podman is reachable. An explicit value is always honored — engine = "podman" never falls back to Docker, and engine = "docker" opts out of the Podman fallback.
# ~/.glci/config.toml
[docker]
engine = "podman"
Podman needs a running API socket (podman system service):
- Rootless:
systemctl --user enable --now podman.socket— socket at$XDG_RUNTIME_DIR/podman/podman.sock - Rootful:
sudo systemctl enable --now podman.socket— socket at/run/podman/podman.sock - macOS / Windows:
podman machine start
On macOS and Windows Podman runs inside a VM, and the default connection is an ssh:// endpoint. glci resolves it from podman system connection list and exports both CONTAINER_HOST (the URI) and CONTAINER_SSHKEY (the connection’s SSH identity) — the identity is required because CONTAINER_HOST overrides the named connection, so without it every subsequent podman call against a healthy machine fails as “not reachable”.
The socket mounted into containers is the path the machine connection itself reports (the ssh:// URI’s path), because that is the path on the side the containers actually run on. A default podman machine is rootless inside the VM, so that is /run/user/<uid>/podman/podman.sock; the rootful /run/podman/podman.sock is only used as a fallback and applies to podman machine init --rootful.
Images that glci pulls for its own infrastructure (the runner container and the QEMU binfmt helper) are fully qualified with docker.io/. Podman ships short-name-mode = "enforcing" in registries.conf, which cannot resolve a bare gitlab/gitlab-runner without a terminal to prompt on and would otherwise fail the run with short-name resolution enforced but cannot prompt without a TTY. Docker treats the prefix as equivalent, so this is engine-agnostic. Images in your .gitlab-ci.yml are passed through verbatim and resolved by Podman’s Docker-compatible API, which applies the usual Docker Hub default.
glci also needs two Podman namespace defaults, both of which a normal installation already has. If containers.conf overrides them (as podman-in-podman images such as quay.io/podman/stable do), pipelines fail:
netnsmust not behost. glci wires the mock, registry and runner together over per-pipeline networks and resolves them by container name. Undernetns="host"every container is host-networked andpodman network connectfails withError: "" is not supported: invalid network mode.utsnsmust not behost. gitlab-runner sets a hostname on each job container, which Podman rejects withcannot set hostname when running in the host UTS namespace.
For remote or named Podman endpoints, [docker] host and the per-runner docker_host/docker_context keys also accept Podman sockets and connection names. Internally, glci uses one of two mutually exclusive paths per endpoint:
- Raw endpoint (
docker_host,[docker] host) — sets the engine’s endpoint environment variable:DOCKER_HOSTfor Docker,CONTAINER_HOSTfor Podman. - Named context/connection (
docker_context) — passes a flag instead:docker --context <name>for Docker,podman --connection <name>for Podman.
Both paths first strip DOCKER_HOST, CONTAINER_HOST, DOCKER_CERT_PATH, and DOCKER_TLS_VERIFY from the command environment. This matters most for the flag path: both engines give the endpoint environment variable precedence over the flag, so an inherited DOCKER_HOST/CONTAINER_HOST would silently redirect a context-based runner to the wrong daemon. With the variables removed, the flag is authoritative.
Per-runner Docker host#
Named runners can override the Docker daemon individually with docker_host (endpoint URL) or docker_context (Docker context name) in [runners.<name>] of ~/.glci/config.toml. Like the [docker] endpoint keys, the per-runner engine, docker_host, docker_context, container_socket, tls_cert_path, and tls_verify keys are global config only — a project .glciconfig.toml that sets them has them dropped with a warning (see Config files > Container-endpoint keys). A per-runner engine = "podman" selects Podman for that runner; because the default daemon is process-global, it is only meaningful alongside a distinct docker_host/docker_context pointing at the Podman endpoint. For tcp:// endpoints requiring TLS, configure tls_cert_path and tls_verify per runner. See Named Runners for configuration details.
tls_cert_path/tls_verify are Docker-only: they map to DOCKER_CERT_PATH/DOCKER_TLS_VERIFY, which Podman does not honor. A tcp:// endpoint on a runner with engine = "podman" therefore carries no TLS even with both keys set, and glci config --network warns about it. Use an ssh:// endpoint instead — Podman’s only authenticated and encrypted remote transport. A podman system connection referenced via docker_context is only as secure as its own URI; a connection pointing at tcp:// is still unencrypted and unauthenticated.
When a named runner targets a daemon on a different machine (not localhost or a unix socket), the network topology changes significantly — the runner and its job containers live on a separate Docker host that can’t directly reach the mock server:
Key differences for remote runners:
- Dedicated bus network: Each remote runner gets its own network (
glci-net-{pipeline}-{runnerName}) on the remote daemon, since it can’t join the local pipeline network - Relay proxy container: A
glci-proxy-*container is deployed on the remote daemon with theglci-mocknetwork alias, so runner and job containers resolveglci-mockvia Docker DNS to the relay - Muxproto relay: The relay serializes runner HTTP requests as binary frames over
docker execstdin/stdout back to the daemon, which forwards them to the real mock server - No direct connectivity needed: The remote host doesn’t need TCP access back to the daemon machine — everything flows through Docker’s exec transport
Docker-in-Docker#
For CI pipelines that use Docker-in-Docker services (docker:dind), glci automatically:
- Mounts the Docker socket for DinD service containers
- Injects a
buildkitd.tomlmarking the mock registry as insecure (HTTP) - Fixes wrong-architecture buildx plugins (e.g. CI downloads amd64 binary but container is arm64)
- Registers QEMU binfmt handlers for cross-platform builds
- Configures every pipeline’s DinD as a pull-through mirror of the embedded registry
- Wraps
docker buildx createto inject--config(registry trust) and--driver-opt network=(pipeline DNS)
Under Podman, rootful DinD works (privileged defaults to true). Rootless Podman DinD is best-effort and unsupported — nested privileged containers are unreliable in a rootless environment, so use a rootful Podman socket for DinD pipelines.
Build layer cache#
glci does not inject BuildKit cache flags automatically. Use --cache-from / --cache-to in your .gitlab-ci.yml as you would on GitLab CI. Since the embedded registry persists across runs, type=registry cache references work without extra setup.
Network configuration#
Fine-tune how containers reach the mock server, registry, and host services:
# ~/.glci/config.toml
[network]
mock_server_bind = "0.0.0.0:0" # mock server listen address (default)
mock_server_port = 39741 # host port for mock server (default); change requires daemon restart
container_host = "host.docker.internal" # hostname containers use for host (default)
host_gateway = "auto" # extra_hosts value for container_host (default)
registry_bind = "127.0.0.1:0" # registry HTTPS listener (default)
registry_http_bind = "0.0.0.0:0" # registry HTTP listener (default)
daemon_socket = "~/.glci/daemon.sock" # Unix socket path (default)
[network.extra_hosts]
entries = ["internal-registry.corp:10.0.0.50"] # additional /etc/hosts entries for containers
The container_host defaults to host.docker.internal, which resolves natively on Docker Desktop (macOS/Windows) and on Linux Docker 20.10+. On older Linux, glci adds an extra_hosts entry mapping it to the Docker bridge IP.
The mock_server_port is the host port that CI job containers use to reach the mock server via Docker’s host-gateway. This port must be available on every Docker host where glci runs. Changing it requires a full daemon restart (glci daemon stop && glci daemon start).
Use glci config --network to see all resolved network settings:
glci config --network
CI cache#
GitLab CI cache: keywords work the same as production, including key: files: (MD5-based), prefix:, and fallback_keys:. Entries persist across pipeline runs via daemon-managed disk storage on the glci-cache container engine volume.
[cache]
persistent = true # default; set false to disable
max_size = "500MB" # reject uploads when exceeded (default: 500MB)
ttl = "24h" # entry expiry (default: 24h)
glci system cache clean # wipe all CI cache entries
Setting persistent = false disables cross-pipeline caching entirely; each pipeline starts with cold caches.
System management#
# Disk usage of all glci resources
glci system df
# Remove unused containers, networks, volumes, and CI cache
glci system prune
# Remove everything including registry data and pipeline history
glci system prune --all
# Skip the confirmation prompt
glci system prune --force
# Manage daemon logs
glci system logs info # show log file path and size
glci system logs clean # remove the log file
# Inspect container-engine resources (Docker or Podman)
glci system engine containers
glci system engine networks
glci system engine volumes
# (aliased as `glci system docker ...` for backward compatibility)
glci system prune (without --all) removes stopped containers, unused networks, orphaned volumes, the bare repo cache, and the daemon log. Persistent daemon volumes (CI cache, registry storage, registry CA) are preserved.
glci system prune --all additionally removes registry data, pipeline history, and all persistent volumes. The daemon is automatically stopped and restarted if it was running.
Daemon#
The daemon runs automatically — you rarely need these commands:
glci daemon status # show PID, uptime, active pipelines, commit, engine + endpoint
glci daemon stop # graceful shutdown (waits for running jobs)
glci daemon start # manually start
glci daemon logs # show last 50 lines
glci daemon logs -F # follow in real time
glci daemon logs -n 100 # show last 100 lines
The daemon auto-stops after 30 minutes of idle (configurable via [daemon] idle_timeout). After make install, the next CLI invocation automatically restarts the daemon if built from a different commit. A changed container-engine endpoint is reported rather than auto-restarted — run glci daemon stop to apply it.
Configure daemon settings in ~/.glci/config.toml:
[daemon]
idle_timeout = "1h" # default: 30m
socket = "~/.glci/daemon.sock"
log_file = "~/.glci/daemon.log"
max_log_size = "50MB" # rotate when log exceeds this size (default: 50MB, "0" to disable)
max_log_files = 3 # rotated backups to keep (default: 3)
Log rotation happens at daemon startup. When daemon.log exceeds max_log_size, it is renamed to daemon.log.1 (shifting older backups up to .N), and a fresh log is started. Set max_log_size = "0" to disable rotation.
Crash recovery#
On startup the daemon checks for pipelines whose Docker containers survived the crash. If both the mock server and runner containers are still alive, the daemon resumes execution — re-attaching to running jobs and dispatching pending ones. Pipelines whose containers died are marked as failed. Orphaned containers are cleaned up automatically.
Version#
glci version
# glci commit abc123def456...
# daemon commit abc123def456... (pid 12345, up 2h15m)