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#
- Registry data lives on the
glci-registryandglci-registry-caDocker volumes (mounted at/registryand/registry-cainsideglci-mock) — content survives daemon restarts and pipeline runs - The registry serves the standard OCI distribution spec at
/v2/*on the mock server port (default 39741), the same port as all other mock server endpoints - Jobs whose
image:orservices[].namereference$CI_REGISTRY/$CI_REGISTRY_IMAGEare rewritten at dispatch time to target a127.0.0.1:<port>loopback address on the Docker host - The mock server port is published as a fixed port (default 39741) on
127.0.0.1for host-sidedocker pull - The registry’s read endpoint is unauthenticated and reachable from Docker-host processes
CI variables#
glci automatically sets these variables so registry operations work without configuration:
| Variable | Value | Purpose |
|---|---|---|
CI_REGISTRY | glci-mock:39741 (inside containers; port configurable via mock_server_port) | Registry address for docker push/docker pull |
CI_REGISTRY_IMAGE | glci-mock:39741/<project-path> | Project-specific image prefix |
CI_REGISTRY_USER | Job-derived | Auth username |
CI_REGISTRY_PASSWORD | Job token | Auth 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#
- Job container requests an image from the embedded registry (e.g.
docker pull glci-mock:39741/library/alpine:latest) - Registry checks local storage for the manifest
- On miss: registry proxies the request to configured upstreams in order
- First upstream that has the manifest wins — manifest and blobs are fetched, stored locally, and returned to the caller
- 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#
- Job runs
docker push $CI_REGISTRY_IMAGE:tag - Mock server’s registry stores the manifest and blobs locally (normal push path)
- 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
HEADprobes 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
- 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:
- Adding or removing push-through for a pipeline does not require a mock container restart
- Upstream credentials (typically a GitLab token) are only known at
glci runtime, not at daemon boot - HTTP connection pools and Bearer token caches are reused across pushes, rebuilt only when upstream/credentials change
Security considerations#
- Scoped to project: only images under the current project’s path are mirrored — multi-pipeline daemons never cross-mirror unrelated projects
- HTTPS only: the upstream must be HTTPS. The
Locationheader host and scheme are validated so a rogue upstream cannot redirect blob PUTs or downgrade to HTTP - Credential safety: the Bearer token cache is scoped to the configured upstream, and the token-endpoint realm is restricted to known-good hosts (gitlab.com, auth.docker.io, or a subdomain of the upstream) so a forged 401 cannot exfiltrate credentials
- Upstream credentials: require
write_registryscope on the token
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:
| Context | How trust is established |
|---|---|
| Job containers | CI_REGISTRY points to glci-mock:39741 over HTTP; the mock is reachable via extra_hosts (host-gateway) and FF_NETWORK_PER_BUILD |
| DinD services | daemon.json with insecure-registries marking the mock’s address |
| Buildx/BuildKit | buildkitd.toml with http = true and insecure = true for the mock’s address |
| Host Docker daemon | Published 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.