Docker & Network

Network topology#

graph TB subgraph host["HOST"] CLI["glci CLI"] Daemon["glci Daemon
talks to Docker via socket / TCP / SSH"] end subgraph pipeline["Per-pipeline network · glci-net-<pipelineID>"] Mock["glci-mock · port 39741
───
Runner API · Git HTTP
S3 cache · OCI registry"] subgraph shared_runner["glci-runner container (shared)"] R1["[[runners]] #1
token-A · tags: gpu"] R2["[[runners]] #2
token-B · tags: default"] RN["[[runners]] #N
…"] end subgraph project_runner["glci-runner-<project> container (optional)"] PR1["[[runners]] #1
per-project token · tags"] end end subgraph build["Per-build network · FF_NETWORK_PER_BUILD"] Job["Job container
user Docker image"] Svc["Service sidecars
postgres, redis, …"] end subgraph dind["Per-DinD-job network · glci-net-<pipelineID>-job-<jobID>"] DJob["DinD job container"] DinDSvc["docker:dind
TLS :2376 · plain :2375"] DMock["glci-mock
(attached to network)"] end CLI -->|"Unix socket
~/.glci/daemon.sock"| Daemon Daemon -.->|"docker exec -i
muxproto binary protocol"| Mock Daemon -.->|"config volume · SIGHUP"| shared_runner Daemon -.->|"config volume · SIGHUP"| project_runner R1 -->|"Docker DNS
glci-mock:39741"| Mock R2 -->|"Docker DNS
glci-mock:39741"| Mock RN -->|"Docker DNS"| Mock PR1 -->|"Docker DNS
glci-mock:39741"| Mock shared_runner -->|"Docker executor"| Job project_runner -->|"Docker executor"| Job shared_runner -->|"Docker executor
(DinD jobs)"| DJob Job -->|"host.docker.internal
host-gateway → :39741"| Mock Job <-.->|"DNS aliases"| Svc DJob -->|"DOCKER_HOST
tcp://docker:2376"| DinDSvc DinDSvc -->|"insecure-registries
pull / push"| DMock DMock -.-|"same container"| Mock style host fill:#f5f5f5,stroke:#666,stroke-width:1px style pipeline fill:#e8f4fd,stroke:#3b82f6,stroke-width:2px style shared_runner fill:#dbeafe,stroke:#60a5fa,stroke-width:1px,stroke-dasharray:5 5 style project_runner fill:#dbeafe,stroke:#60a5fa,stroke-width:1px,stroke-dasharray:5 5 style build fill:#ecfdf5,stroke:#10b981,stroke-width:2px style dind fill:#fef3c7,stroke:#f59e0b,stroke-width:2px

Three network layers exist at runtime:

NetworkScopeContainersPurpose
glci-net-{pipelineID}Per pipelineglci-mock, glci-runner, glci-runner-<project> (if any)Runners poll mock for jobs via Docker DNS (glci-mock:39741)
Per-build networkPer jobJob container, service sidecarsService DNS aliases (postgres, redis, etc.)
glci-net-{pipelineID}-job-{jobID}Per DinD jobDinD job, docker:dind, glci-mock (attached)Isolates DinD docker alias; gives DinD access to the embedded registry

How each component reaches the mock server:

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, and privileged are honored only in ~/.glci/config.toml. A project .glciconfig.toml that 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. privileged is 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:

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):

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:

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:

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:

graph TB subgraph local["Daemon machine (e.g. Mac with colima)"] Daemon["glci daemon"] subgraph local_net["glci-net-<pipelineID>"] Mock["glci-mock
:39741"] LocalRunner["glci-runner (local)"] end LocalJob["Local job containers"] end subgraph remote["Remote Docker host (e.g. Linux server)"] subgraph remote_net["glci-net-<pipelineID>-<runnerName>"] Relay["glci-proxy-*
relay container
(alias: glci-mock)"] RemoteRunner["glci-runner-*-<name>"] end RemoteJob["Remote job containers"] end Daemon -.->|"docker exec -i
muxproto"| Mock Daemon -.->|"docker exec -i
muxproto"| Relay LocalRunner -->|"Docker DNS
glci-mock:39741"| Mock LocalRunner -->|"Docker executor"| LocalJob LocalJob -->|"host-gateway
:39741"| Mock RemoteRunner -->|"Docker DNS
glci-mock:39741"| Relay RemoteRunner -->|"Docker executor"| RemoteJob RemoteJob -->|"host-gateway
→ bus network gateway
→ relay"| Relay Relay <-.->|"muxproto over
docker exec stdin/stdout"| Daemon style local fill:#ecfdf5,stroke:#10b981,stroke-width:2px style remote fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style local_net fill:#e8f4fd,stroke:#3b82f6,stroke-width:1px style remote_net fill:#fde68a,stroke:#f59e0b,stroke-width:1px

Key differences for remote runners:

Docker-in-Docker#

For CI pipelines that use Docker-in-Docker services (docker:dind), glci automatically:

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)
Esc