Docker-in-Docker

glci has comprehensive support for CI pipelines that use Docker-in-Docker services (docker:dind).

Automatic configuration#

When a job uses DinD services, glci automatically:

  1. Mounts the Docker socket for DinD service containers
  2. Injects buildkitd.toml marking the mock registry as insecure (HTTP) so buildx builders trust it
  3. Injects daemon.json with insecure-registries so DinD’s dockerd trusts the mock registry
  4. Handles TLS setup — waits for DinD to generate TLS client certs, configures DOCKER_CERT_PATH, DOCKER_TLS_VERIFY, and DOCKER_HOST with the correct TLS port
  5. Runs a daemon readiness gate — polls docker info until DinD is fully accepting connections, eliminating races where cert files exist but the daemon has not finished TLS initialization
  6. Fixes architecture mismatches — e.g. CI downloads amd64 buildx binary but container runs arm64
  7. Registers QEMU binfmt handlers for cross-platform builds (one-time, idempotent)
  8. Configures pull-through mirroring — each pipeline’s DinD acts as a pull-through mirror of the embedded registry, so images pulled in one pipeline are cached for the next
  9. Wraps docker buildx create to inject --config (registry trust) and --driver-opt network= (pipeline DNS) so custom builders can reach the embedded registry
  10. Clears stale buildx state — removes ~/.docker/buildx/ inside the job container to prevent the docker image’s default builder config from conflicting with DinD TLS

Isolation model#

Each pipeline gets its own ephemeral /var/lib/docker — no shared mount, no mutex, no serialization. Concurrent DinD pipelines run in parallel without interference.

Per-job DinD networks#

Each DinD job within a pipeline gets its own Docker network (glci-net-{pipelineID}-job-{jobID}). This prevents a critical issue: when multiple DinD jobs run concurrently, they would all register a docker service alias. Docker’s embedded DNS would round-robin between the DinD daemons, causing TLS cert verification failures and unpredictable behavior. Per-job networks ensure each job’s docker hostname resolves to exactly one DinD instance.

The mock server container is attached to each per-job network so the DinD daemon can reach the embedded registry.

graph LR subgraph net1["glci-net-{id}-job-1"] Job1["Job 1"] DinD1["docker:dind"] Mock1["glci-mock"] Job1 -->|"docker:2376"| DinD1 DinD1 -->|"registry"| Mock1 end subgraph net2["glci-net-{id}-job-2"] Job2["Job 2"] DinD2["docker:dind"] Mock2["glci-mock"] Job2 -->|"docker:2376"| DinD2 DinD2 -->|"registry"| Mock2 end Mock1 -.- Same["Same glci-mock container
attached to both networks"] Mock2 -.- Same style net1 fill:#e8f4fd,stroke:#3b82f6,stroke-width:2px style net2 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style Same fill:none,stroke:none

Each job’s docker hostname resolves to its own DinD instance — no cross-talk, no DNS round-robin. The single glci-mock container is attached to all per-job networks simultaneously so every DinD daemon can pull from and push to the embedded registry.

TLS and DinD#

glci handles the full DinD TLS lifecycle automatically. Here is what happens when a job with DOCKER_TLS_CERTDIR: "/certs" starts:

  1. DinD service container starts generating TLS certificates at /certs
  2. glci’s pre-build script polls for ca.pem, cert.pem, and key.pem to appear (up to 30 seconds)
  3. Once certs are found, the script sets DOCKER_CERT_PATH, DOCKER_TLS_VERIFY=1, and fixes DOCKER_HOST to use port 2376 (TLS)
  4. A readiness gate polls docker info until the daemon accepts TLS connections
  5. Job scripts can then use docker commands normally

This matches the standard GitLab.com DinD setup where DOCKER_TLS_CERTDIR=/certs.

Buildx and BuildKit#

glci wraps docker buildx create to transparently inject configuration that makes buildx work with the mock registry and pipeline network.

What glci injects#

ConfigurationValuePurpose
--config/glci/buildkitd.tomlMarks the mock registry as HTTP + insecure
--driver-opt network=Pipeline network nameGives the builder DNS access to glci-mock
Fallback for DinD--driver-opt network=hostInside DinD, the per-job network does not exist on the inner Docker daemon, so the builder shares the DinD container’s network namespace

The wrapper is injected via pre_build_script in the runner config. It intercepts docker buildx create commands (and docker build --builder) and adds the required flags, leaving all user-specified flags intact.

Stale buildx state#

The docker:* images ship with ~/.docker/buildx/current pointing to unix:///var/run/docker.sock. When DOCKER_HOST is tcp://docker:2376 (DinD with TLS), the mismatch causes buildx to auto-create a builder with the docker-container driver, which spins up a separate BuildKit container that cannot verify the DinD TLS certificates. glci’s pre-build script removes ~/.docker/buildx/ to let buildx initialize fresh from DOCKER_HOST.

QEMU and cross-platform builds#

glci automatically registers QEMU binfmt handlers the first time a DinD job runs. This enables buildx to build images for architectures other than the host (e.g. amd64 on Apple Silicon, arm64 on amd64):

# What glci runs automatically (idempotent, cached after first success):
docker run --rm --privileged tonistiigi/binfmt --install all

If QEMU registration fails, the daemon logs a warning and cross-arch builds may fail. You can run the command manually to fix it.

Build layer cache#

glci does not inject cache flags automatically. To enable BuildKit layer caching across pipelines, use --cache-from / --cache-to with the embedded registry as the cache backend. The registry volume persists across daemon restarts, so cached layers survive between pipelines. No additional trust configuration is needed – the registry is already trusted by buildx via the injected buildkitd.toml.

docker-build:
  image: docker:27
  services: [docker:27-dind]
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker buildx create --use
    - docker buildx build
      --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/cache:latest"
      --cache-to "type=registry,ref=$CI_REGISTRY_IMAGE/cache:latest,mode=max"
      -t "$CI_REGISTRY_IMAGE:latest"
      --push .

Standard DinD pipelines (basic builds, multi-platform builds, no-TLS builds) work the same as on GitLab CI with no glci-specific changes needed. QEMU binfmt handlers are registered automatically for cross-platform builds.

Troubleshooting DinD#

docker: command not found#

The job container needs the Docker CLI installed. Use a docker:* image or install the CLI in your Dockerfile.

Cannot connect to the Docker daemon#

CauseFix
DinD service not started yetglci’s pre-build script includes a readiness gate. If it still fails, increase the timeout or add a manual wait.
Wrong DOCKER_HOSTCheck if DOCKER_TLS_CERTDIR is set. With TLS, DOCKER_HOST should be tcp://docker:2376. Without TLS, tcp://docker:2375. glci’s pre-build script handles this automatically.
DinD container crashedCheck glci daemon logs for DinD container errors.

x509: certificate signed by unknown authority#

The DinD daemon or buildx does not trust the mock registry.

ContextWhat glci doesWhat to check
docker push/pulldaemon.json with insecure-registriesCheck glci daemon logs for “could not generate daemon.json”
docker buildx buildbuildkitd.toml with http = trueCheck glci daemon logs for “could not generate buildkitd config”

Restarting the daemon usually fixes this: glci daemon stop --force && glci daemon start.

exec format error#

The image architecture does not match the host. QEMU binfmt handlers may not be registered:

# Register manually
docker run --privileged --rm tonistiigi/binfmt --install all

On Apple Silicon with Colima, use Rosetta for better amd64 emulation:

colima stop
colima delete
colima start --vm-type=vz --vz-rosetta --cpu 12 --memory 16

Concurrent DinD jobs interfere with each other#

This should not happen with glci’s per-job network isolation. If it does, check that the per-job networks are being created:

docker network ls --filter name=glci-net-

Each DinD job should have its own glci-net-{pipelineID}-job-{jobID} network.

How it differs from production GitLab CI#

AspectglciProduction GitLab
DinD /var/lib/dockerEphemeral per pipelineEphemeral per job
Registry trustdaemon.json + buildkitd.toml injectedRegistry uses the same CA as the runner
QEMU binfmtRegistered on host Docker daemonPre-configured on runner hosts
Buildx wrapperInjected via pre_build_scriptNot needed (standard Docker setup)
Network isolationPer-job Docker network for DinDRunner manages networking
TLS setupPre-build script with cert wait + readiness gateDinD service entrypoint handles it
Esc