GitLab CI DinD: Docker builds with BuildKit caching
Published: 2026-03-25
Docker-in-Docker (DinD) in GitLab CI lets you build and push container images as part of your pipeline. The setup involves TLS, a Docker daemon service, and a shared volume for the TLS certificates. With BuildKit layer caching pointed at a registry, incremental builds take 30–60 seconds instead of 5 minutes.
Basic DinD setup
yaml# .gitlab-ci.yml
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_BUILDKIT: "1"
REGISTRY: registry.example.com
services:
- name: docker:26-dind
command: ["--registry-mirror", "https://mirror.example.com"]
build:
image: docker:26
stage: build
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$REGISTRY"
script:
- |
docker buildx build \
--cache-from type=registry,ref=${REGISTRY}/${CI_PROJECT_NAME}:cache \
--cache-to type=registry,ref=${REGISTRY}/${CI_PROJECT_NAME}:cache,mode=max \
--platform linux/amd64 \
--tag ${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA} \
--tag ${REGISTRY}/${CI_PROJECT_NAME}:latest \
--push \
.
The docker:dind service and docker:26 image must use the same major version. DOCKER_TLS_CERTDIR: "/certs" enables TLS between the client and daemon automatically — the client reads certs from /certs/client, the daemon writes them.
The TLS certificate problem
Without the shared DOCKER_TLS_CERTDIR, the Docker CLI can't authenticate to the daemon and you get:
Cannot connect to the Docker daemon at tcp://docker:2376.
Fix: set DOCKER_TLS_CERTDIR: "/certs" and GitLab handles the volume mount between the service and job container automatically. Both containers see /certs.
If TLS is blocking you during development, disable it with DOCKER_TLS_CERTDIR: "" and use tcp://docker:2375 — but never in production.
Registry mirror
A registry mirror is a pull-through cache for Docker Hub images. Without it, every DinD runner pulls alpine:3.20 fresh from Docker Hub, hitting rate limits and adding 30–60 seconds per pull.
yamlservices:
- name: docker:26-dind
command: ["--registry-mirror", "https://mirror.example.com"]
mirror.example.com is your own Registry proxy — registry:2 image configured with a proxy:
yaml# registry config.yml
proxy:
remoteurl: https://registry-1.docker.io
BuildKit inline caching vs registry caching
Two modes for --cache-to:
inline — embeds cache metadata in the image layer. Simple but forces you to pull the full image to get cache data.
registry — stores cache as a separate manifest. The cache tag is small (just metadata + diff layers), pulled quickly. Use mode=max to cache all stages, not just the final one.
For a multi-stage .NET build:
restorestage rarely changes (only when.csprojchanges)buildstage changes on every commit- With
mode=max, therestorestage is cached even on cold runners
Multi-arch builds
yamlbefore_script:
- docker run --privileged --rm tonistiigi/binfmt --install all
- docker buildx create --use --name multiarch
script:
- |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=${REGISTRY}/${CI_PROJECT_NAME}:cache \
--cache-to type=registry,ref=${REGISTRY}/${CI_PROJECT_NAME}:cache,mode=max \
--tag ${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA} \
--push \
.
binfmt registers QEMU emulators for non-native architectures. Expect 3–5× longer builds for the emulated architecture.
Trivy scan in the same pipeline
yamlscan:
image:
name: aquasec/trivy:latest
entrypoint: [""]
stage: scan
needs: [build]
script:
- |
trivy image \
--format json \
--output trivy-report.json \
--severity HIGH,CRITICAL \
--exit-code 1 \
${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA}
artifacts:
when: always
reports:
container_scanning: trivy-report.json
--exit-code 1 fails the pipeline on HIGH/CRITICAL vulnerabilities. The container_scanning artifact shows up in the GitLab MR security tab.
Kaniko as an alternative
If you can't use privileged containers (some Kubernetes setups), use Kaniko instead:
yamlbuild:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
script:
- |
/kaniko/executor \
--context . \
--dockerfile Dockerfile \
--destination ${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA} \
--cache=true \
--cache-repo=${REGISTRY}/${CI_PROJECT_NAME}:cache
Kaniko doesn't require privileged mode and builds inside an unprivileged container. The tradeoff: slower than BuildKit and no multi-arch support.