Embedded Registry

The mock server embeds an OCI registry on the same port (default :39741, configurable via mock_server_port). It’s auto-configured with CI_REGISTRY_* variables so docker build/docker push against $CI_REGISTRY work identically to GitLab.com.

How it works#

CI variables#

glci automatically sets these variables so registry operations work without configuration:

VariableValuePurpose
CI_REGISTRYglci-mock:39741 (inside containers; port configurable via mock_server_port)Registry address for docker push/docker pull
CI_REGISTRY_IMAGEglci-mock:39741/<project-path>Project-specific image prefix
CI_REGISTRY_USERJob-derivedAuth username
CI_REGISTRY_PASSWORDJob tokenAuth password (HMAC-derived per-job token)

Data flow#

Job container                    Mock server (glci-mock:39741)
    |                                |
    |-- docker push $CI_REGISTRY --->|  Manifest + blobs stored
    |                                |  in /registry volume
    |                                |
    |-- docker pull $CI_REGISTRY --->|  Served from /registry volume
    |                                |

The mock’s registry volume is the persistent store. There is no separate sync step — images pushed by one pipeline are immediately available to the next.

Host-side access#

The daemon uses the configured mock_server_port (default 39741) directly and rewrites CI_REGISTRY references in job images to 127.0.0.1:<port> so the Docker daemon on the host can pull images directly. This rewrite happens at dispatch time and is transparent to the job.

Pull-through cache#

The embedded registry also acts as a read-through cache for upstream registries (default: registry.gitlab.com,registry-1.docker.io). Manifest/blob GETs that miss locally are transparently fetched from the first upstream that has them, written back to the volume, and served.

This means repeat pulls of the same CI image or Docker Hub base image across pipelines stay on localhost after the first fetch.

How pull-through works#

  1. Job container requests an image from the embedded registry (e.g. docker pull glci-mock:39741/library/alpine:latest)
  2. Registry checks local storage for the manifest
  3. On miss: registry proxies the request to configured upstreams in order
  4. First upstream that has the manifest wins — manifest and blobs are fetched, stored locally, and returned to the caller
  5. Subsequent pulls for the same image hit local storage directly

Configuring upstreams#

The default pull-through upstreams are registry.gitlab.com and registry-1.docker.io. DinD services within a pipeline are configured to use the embedded registry as a mirror, so images pulled inside DinD are also cached.

Push-through mirror#

When enabled, every image pushed to CI_REGISTRY is synchronously mirrored to the upstream registry (usually registry.gitlab.com). This lets you build and test images locally, then publish them to the real registry in the same pipeline run.

Enabling push-through#

Globally via config:

# ~/.glci/config.toml
[registry]
push_through = true

[registry.upstream]
username = "deploy-token"
password = "$REGISTRY_WRITE_TOKEN"   # env var reference

Or per-run:

glci run --push-through

How push-through works#

  1. Job runs docker push $CI_REGISTRY_IMAGE:tag
  2. Mock server’s registry stores the manifest and blobs locally (normal push path)
  3. On each manifest PUT, the push-through hook fires synchronously:
    • Sub-manifests of a manifest list/index are pushed first (bottom-up) so the upstream is always consistent when the top-level manifest arrives
    • Blobs are deduplicated via HEAD probes against the upstream — already-present blobs are a no-op
    • Blobs and manifests are read from the mock’s local storage and uploaded to the upstream
  4. If the upstream push fails, the error surfaces to the job as a 5xx from docker push

Push-through architecture#

The daemon sends push-through configuration to the mock server via POST /internal/push-through-config. The mock server’s sidecar process receives the config and live-swaps its settings. Additionally, the sidecar polls a local file (push-through.json on the glci-registry-ca volume) every 2 seconds for config changes. This means:

Security considerations#

Registry management#

# List images in the registry
glci registry list

# Show registry disk usage
glci registry stats

# Pull an image from the embedded registry to the host
glci registry pull <image>

# Clean up all registry data
glci registry clean

The glci system df command also shows registry disk usage alongside cache and artifact storage.

TLS and trust#

The embedded registry serves HTTP (not HTTPS) within the pipeline’s Docker network. Trust is configured automatically:

ContextHow trust is established
Job containersCI_REGISTRY points to glci-mock:39741 over HTTP; the mock is reachable via extra_hosts (host-gateway) and FF_NETWORK_PER_BUILD
DinD servicesdaemon.json with insecure-registries marking the mock’s address
Buildx/BuildKitbuildkitd.toml with http = true and insecure = true for the mock’s address
Host Docker daemonPublished port on 127.0.0.1 (loopback) — no TLS needed for local access

See Docker-in-Docker for how DinD and buildx trust is established.

Esc