Docker · GitLab CI · Runner · DinD · Caching · Artifacts
Docker in GitLab CI
Runner, DinD, Caching und Artifacts

GitLab CI mit Docker ist mächtig, aber die Konfiguration hat viele Fallstricke: falsche Runner-Typen, unsicheres Docker-in-Docker, Build-Caches die nicht treffen und Artifacts, die unnötige Megabytes transferieren. Wer die Mechanismen versteht, baut Pipelines, die in drei Minuten fertig sind statt in dreißig.

13 Min. Lesezeit Runner · DinD · Socket-Mounting · BuildKit · Registry-Cache GitLab 17+ · Docker Engine 25+ · BuildKit

1. GitLab Runner Typen: Shell, Docker und Kubernetes

Der GitLab CI-Runner ist die Komponente, die Pipeline-Jobs ausführt. Es gibt drei wichtige Executor-Typen. Der Shell-Executor führt Jobs direkt auf dem Runner-Host aus – kein Container, keine Isolation. Das ist schnell, aber jeder Job kann den Zustand des nächsten beeinflussen. Der Docker-Executor erstellt für jeden Job einen Container aus dem in image: definierten Docker-Image und wirft ihn nach dem Job weg. Das gibt vollständige Isolation: jeder Job startet in einem sauberen Zustand. Der Kubernetes-Executor startet Jobs als Pods im Cluster – ideal für bereits vorhandene K8s-Infrastruktur.

Für Docker in GitLab CI ist der Docker-Executor die häufigste Wahl. Die Runner-Konfiguration in /etc/gitlab-runner/config.toml definiert, welches privileged-Flag der Docker-Executor erhält und welche Volumes gemountet werden. Privileged-Mode ist für Docker-in-Docker nötig, aber ein Sicherheitsrisiko – ein Container mit privileged-Flag kann auf dem Host eskalieren. Self-hosted Runner sollten auf dedizierten Hosts laufen, nicht auf denselben Systemen, auf denen Produktions-Workloads laufen. GitLab.com-SaaS-Runner nutzen isolierte VMs für jeden Job und haben kein privileged-Problem.

2. Docker-in-Docker: wann es nötig ist und wann nicht

Docker-in-Docker (DinD) bedeutet, einen Docker-Daemon innerhalb eines Docker-Containers zu betreiben. Das ist notwendig, wenn ein GitLab CI-Job Docker-Commands ausführen muss – zum Beispiel, um Images zu bauen – und der Runner-Container selbst keinen Zugriff auf den Host-Docker-Daemon hat. DinD benötigt den docker:dind-Service-Container, der den Docker-Daemon bereitstellt, und das privileged: true-Flag im Runner, damit der Service-Container auf den nötigen Kernel-Funktionen aufbauen kann. Ohne privileged läuft DinD nicht.

Die Sicherheitsimplikation von DinD in GitLab CI: ein privilegierter Container hat volle Zugriffsrechte auf den Host-Kernel. Jeder CI-Job, der in einem privilegierten Container läuft, kann im Prinzip aus dem Container ausbrechen. Das ist auf dedizierten CI-Runner-Hosts akzeptabel – das Risiko ist auf diesen Host begrenzt. Auf Hosts, die andere Workloads laufen, ist es inakzeptabel. Eine echte Alternative ist der kaniko-Build-Container von Google, der Docker-Images ohne Docker-Daemon und ohne privileged-Flag baut – allerdings langsamer und ohne vollständigen BuildKit-Support.


# .gitlab-ci.yml — Docker image build with Docker-in-Docker
# Requires privileged Runner; use on dedicated CI hosts only

variables:
  DOCKER_HOST: tcp://docker:2376
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_BUILDKIT: "1"                    # enable BuildKit for parallel stages and cache
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

build:
  stage: build
  image: docker:26
  services:
    - name: docker:26-dind
      variables:
        DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    # Login to GitLab Container Registry using built-in CI variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    # Build with registry cache — speeds up rebuilds significantly
    - |
      docker buildx build \
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache:main \
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE/cache:main,mode=max \
        --tag $IMAGE_TAG \
        --push \
        .
  after_script:
    - docker logout "$CI_REGISTRY"

3. Socket-Mounting als DinD-Alternative

Socket-Mounting ist die häufigste Alternative zu DinD in Docker GitLab CI-Setups. Dabei wird der Docker-Socket des Hosts (/var/run/docker.sock) als Volume in den Job-Container gemountet. Der Container kann dann Docker-Befehle ausführen, die direkt auf dem Host-Docker-Daemon laufen – ohne einen eigenen privilegierten Daemon. Das ist einfacher als DinD, aber hat einen wichtigen Unterschied: alle Jobs teilen denselben Docker-Daemon, was zu Race Conditions bei gleichnamigen Images oder Container-Namen führen kann.

Das Sicherheitsmodell von Socket-Mounting in GitLab CI ist problematisch: ein Job mit Zugriff auf /var/run/docker.sock kann beliebige Container auf dem Host starten, Volumes mounten und damit effektiv Root-Zugriff auf den Host erlangen. Das ist mindestens genauso problematisch wie DinD mit privileged-Flag. Der einzige wirklich sichere Ansatz für Docker in GitLab CI auf geteilter Infrastruktur ist kaniko oder Buildah, die keine Daemon-Verbindung benötigen. Auf dedizierten CI-Hosts ist Socket-Mounting pragmatisch und weit verbreitet.


# GitLab Runner config.toml — Socket mounting configuration
# Use on dedicated CI hosts only — socket access grants host root equivalence

[[runners]]
  name = "docker-runner-socket"
  executor = "docker"
  [runners.docker]
    image = "docker:26"
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    # No privileged = true needed for socket mounting
    # But: any job can start arbitrary containers on the host

---

# .gitlab-ci.yml — Build job using socket-mounted Docker daemon

build-socket:
  stage: build
  image: docker:26
  variables:
    DOCKER_HOST: "unix:///var/run/docker.sock"    # use host daemon via socket
    DOCKER_BUILDKIT: "1"
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    # Use unique tag based on commit SHA to avoid naming conflicts between parallel jobs
    - docker build --tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    # Tag as latest only on default branch
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
        docker push "$CI_REGISTRY_IMAGE:latest"
      fi

4. BuildKit in GitLab CI aktivieren

BuildKit ist seit Docker Engine 23 Standard, muss aber in GitLab CI-Jobs explizit aktiviert werden, wenn der Job-Container eine ältere Docker-Version nutzt oder die Umgebungsvariable nicht gesetzt ist. Die Aktivierung erfolgt über die Variable DOCKER_BUILDKIT=1 im variables-Block. Mit BuildKit aktiviert, werden Multi-Stage-Builds parallelisiert, der Registry-Cache funktioniert mit --cache-from type=registry und Mount-Types für Secrets und Caches stehen zur Verfügung. BuildKit verbessert die Build-Performance von Docker in GitLab CI bei Multi-Stage-Builds erheblich.

Eine wichtige BuildKit-Funktion für GitLab CI ist das Inline-Caching. Mit --build-arg BUILDKIT_INLINE_CACHE=1 werden Cache-Metadaten direkt ins Image eingebettet. Das ermöglicht es, ein bestehendes Image als Cache-Quelle zu nutzen, ohne einen separaten Cache-Export: --cache-from $CI_REGISTRY_IMAGE:main nutzt dann das bereits gepushte Main-Branch-Image als Cache-Basis für Feature-Branch-Builds. Das ist weniger effizient als der vollständige Registry-Cache-Export, aber einfacher zu konfigurieren und ausreichend für viele Teams.

5. Registry-based Build-Cache für schnelle Rebuilds

Der Registry-based Build-Cache ist die effektivste Methode, um Docker in GitLab CI-Builds zu beschleunigen. Ohne Cache werden alle Dockerfile-Schritte bei jedem Pipeline-Run von Null ausgeführt: Package-Installation, Dependency-Download, Compiler-Läufe. Mit Registry-Cache werden gecachte Layer aus der Registry geladen und wiederverwendet – nur die tatsächlich geänderten Schritte laufen neu. Die Cache-Hit-Rate hängt davon ab, wie gut das Dockerfile für maximales Caching optimiert ist.

Die empfohlene Cache-Strategie für GitLab CI mit Docker: ein separates Cache-Repository in der GitLab-Registry ($CI_REGISTRY_IMAGE/cache), das unabhängig von den eigentlichen Images befüllt wird. Cache wird mit mode=max exportiert – das speichert alle Layers aller Stages, nicht nur die finale Stage. Feature-Branch-Builds nutzen den Main-Branch-Cache als Ausgangspunkt. Der Cache wird nach jedem Main-Branch-Build aktualisiert. Dieses Muster stellt sicher, dass Feature-Branches von einem warmen Cache profitieren, ohne dass parallele Branches sich gegenseitig überschreiben.

6. Pipeline-Struktur für Docker-Builds

Eine gut strukturierte GitLab CI-Pipeline für Docker-Builds hat klare Stages mit definierten Verantwortlichkeiten: build für den Image-Build und Push in die Registry, test für Tests die den gebauten Container nutzen, scan für Sicherheits-Scanning, und deploy für Deployments auf Zielumgebungen. Jeder Stage-Job nutzt das Image des vorherigen Stages – nicht den Source-Code neu auschecken und neu bauen. Das stellt sicher, dass Test, Scan und Deploy dasselbe Image verwenden, das in Production landet.

Dependency-Jobs in GitLab CI steuern, welcher Job auf welchen anderen warten muss. Mit needs: statt dependencies: werden direkte Job-Abhängigkeiten definiert, unabhängig von der Stage-Reihenfolge. Das ermöglicht, unabhängige Jobs parallel laufen zu lassen: Image-Build und Static-Code-Analysis können gleichzeitig laufen, der Test-Job wartet auf beide. Diese Parallelisierung reduziert die Gesamtlaufzeit der Pipeline erheblich – wichtiger als die Build-Zeit des einzelnen Docker-Jobs.


# .gitlab-ci.yml — Complete Docker CI pipeline with caching, scanning and deployment

stages:
  - build
  - test
  - scan
  - deploy

variables:
  DOCKER_BUILDKIT: "1"
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  CACHE_IMAGE: $CI_REGISTRY_IMAGE/cache:$CI_COMMIT_REF_SLUG

build-image:
  stage: build
  image: docker:26
  services: [docker:26-dind]
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    # Use branch-specific cache, fallback to main branch cache
    - |
      docker buildx build \
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE/cache:main \
        --cache-from type=registry,ref=$CACHE_IMAGE \
        --cache-to type=registry,ref=$CACHE_IMAGE,mode=max \
        --tag $IMAGE \
        --push .

trivy-scan:
  stage: scan
  image: aquasec/trivy:latest
  needs: [build-image]
  script:
    # Fail pipeline on critical CVEs; generate GitLab Security Dashboard report
    - trivy image --exit-code 1 --severity CRITICAL --format gitlab $IMAGE > gl-container-scanning-report.json || true
  artifacts:
    reports:
      container_scanning: gl-container-scanning-report.json

deploy-staging:
  stage: deploy
  image: alpine/k8s:1.30
  needs: [build-image, trivy-scan]
  script:
    - kubectl set image deployment/app app=$IMAGE -n staging
    - kubectl rollout status deployment/app -n staging
  environment:
    name: staging
  only:
    - main

7. Artifacts richtig einsetzen

Artifacts in GitLab CI sind Dateien, die ein Job erzeugt und die für nachfolgende Jobs oder den Download verfügbar gemacht werden. Der häufigste Fehler: zu viele oder zu große Artifacts. Jedes Artifact wird komprimiert, in die GitLab-Datenbank geschrieben und für jeden nachfolgenden Job übertragen. Wenn ein Build-Job den gesamten Build-Output als Artifact speichert, summiert sich das schnell auf hunderte Megabytes pro Pipeline-Run. Das verlangsamt die Pipeline und belastet den GitLab-Speicher.

Die richtige Strategie für Docker in GitLab CI: Images in die Registry pushen statt als Artifacts speichern. Der gebaute Docker-Container ist das Artifact – er lebt in der Registry mit dem Commit-SHA-Tag. Nachfolgende Jobs ziehen das Image aus der Registry statt es als Datei-Artifact zu empfangen. Wirkliche Datei-Artifacts in der Pipeline sollten klein und gezielt sein: Test-Reports, Coverage-Dateien, Security-Scan-Reports und Deployment-Logs. Diese sind typischerweise unter einem Megabyte und wertvoll für den GitLab-Berichts-Mechanismus.

8. DinD vs. Socket-Mounting im Vergleich

Die Wahl zwischen DinD und Socket-Mounting in Docker GitLab CI ist eine der häufigsten Konfigurationsentscheidungen. Beide Ansätze haben klare Vor- und Nachteile.

Kriterium Docker-in-Docker (DinD) Socket-Mounting Empfehlung
Isolation Eigener Daemon pro Job Geteilter Host-Daemon DinD für Isolation
Sicherheit privileged: true nötig Docker-Socket = root-Äquivalenz Beide nur auf dedizierten Hosts
Performance Langsamer Start (Daemon) Schnellerer Start Socket für schnelle Builds
TLS TLS zwischen Job und Daemon Kein TLS, lokaler Socket DinD für TLS-Sicherheit
BuildKit Cache Vollständig unterstützt Vollständig unterstützt Beide gleich

Die Praxisempfehlung für Docker in GitLab CI: DinD auf dedizierten Runner-Hosts mit aktiviertem TLS. Socket-Mounting ist schneller, aber der geteilte Daemon erzeugt bei parallelen Jobs mit ähnlichen Image-Namen gelegentlich Konflikte. Mit der DinD-TLS-Konfiguration (DOCKER_TLS_CERTDIR: "/certs") ist DinD seit Docker 20.10 erheblich sicherer als früher.

9. Sicherheit: was nie in die CI-Pipeline gehört

Sicherheitsfehler in GitLab CI-Pipelines mit Docker sind oft trivial zu vermeiden, kommen aber regelmäßig vor. Credentials gehören niemals in die .gitlab-ci.yml oder in das Repository selbst. GitLab CI Variables (unter Settings → CI/CD → Variables) speichern Secrets verschlüsselt und maskieren sie in Logs. Das $CI_REGISTRY_PASSWORD-Variable ist immer automatisch verfügbar und gehört nie in eine Datei im Repository. API-Keys, Deploy-Tokens und Zugangsdaten für externe Services werden als Masked und Protected Variable angelegt.

Ein weiterer kritischer Sicherheitspunkt in Docker GitLab CI: gepushte Images müssen auf bekannte Schwachstellen gescannt werden, bevor sie deployed werden. Ein Trivy-Scan-Job, der nach dem Build-Job läuft und den Deploy-Job bei kritischen CVEs blockiert, ist eine wichtige Sicherheitsschicht. Das GitLab Security Dashboard zeigt die Scan-Ergebnisse strukturiert an, wenn der Job einen Report im GitLab-Format erzeugt. Das erfordert keine externe Scanning-Infrastruktur – Trivy läuft als Container im CI-Job und benötigt nur den Registry-Zugriff auf das gebaute Image.

10. Zusammenfassung

Docker in GitLab CI effizient zu nutzen erfordert die richtige Runner-Konfiguration, eine klare Entscheidung zwischen DinD und Socket-Mounting und einen gut konfigurierten Build-Cache. Der Docker-Executor mit DinD auf dedizierten Hosts ist die sicherste Option für isolierte CI-Jobs. BuildKit mit Registry-based Cache reduziert Build-Zeiten erheblich: nur veränderte Dockerfile-Schritte laufen neu. Images in die Registry pushen statt als Artifacts – nachfolgende Jobs ziehen das Image aus der Registry.

Eine gute GitLab CI-Pipeline für Docker hat klare Stages: Build, Test, Scan, Deploy. Jeder Stage-Job nutzt das gepushte Image aus der vorherigen Stage. Trivy-Scanning nach dem Build blockiert Deployments bei kritischen CVEs. Credentials nie in der Pipeline-Definition, sondern ausschließlich in GitLab CI Variables. Mit diesen Grundsätzen läuft eine Docker GitLab CI-Pipeline schnell, sicher und reproduzierbar – ohne Überraschungen in Produktion.

Mironsoft

GitLab CI/CD, Docker-Pipelines und Deployment-Automatisierung

Langsame oder fragile GitLab CI Pipeline?

Wir analysieren bestehende GitLab CI Pipelines, konfigurieren BuildKit mit Registry-Cache, richten Image-Scanning ein und bringen Docker-Builds von 20 Minuten auf unter 5 Minuten.

Pipeline-Audit

Analyse langsamer Jobs, ineffizienter Caching-Strategien und Sicherheitslücken in GitLab CI

BuildKit-Optimierung

Registry-Cache, parallele Stages und Dockerfile-Reihenfolge für maximale Cache-Hit-Rate

Security-Setup

Trivy-Scanning, Security Dashboard Integration und sichere Variable-Verwaltung

Docker in GitLab CI — Das Wichtigste auf einen Blick

DinD vs. Socket

DinD: eigener Daemon pro Job, bessere Isolation, TLS-Sicherheit, braucht privileged. Socket: schneller, aber geteilter Daemon und keine Job-Isolation.

Registry-Cache

--cache-from type=registry + --cache-to mode=max. Feature-Branches nutzen Main-Branch-Cache. Spart 80% der Build-Zeit bei Cache-Hits.

Artifacts

Images in Registry pushen, nicht als Artifacts. Artifacts nur für Test-Reports, Coverage und Security-Scan-Ergebnisse – typisch unter 1 MB.

Sicherheit

Credentials nur als GitLab CI Variables. Trivy-Scanning nach Build. Kritische CVEs blockieren Deploy. Runner nur auf dedizierten Hosts.

11. FAQ: Docker in GitLab CI

1DinD vs. Socket-Mounting: Hauptunterschied?
DinD: eigener Daemon pro Job, vollständige Isolation, braucht privileged. Socket: schneller, geteilter Daemon, keine Job-Isolation.
2Warum DinD privileged: true braucht?
Daemon braucht Kernel-Capabilities (cgroups, namespaces), die ohne privileged nicht verfügbar sind.
3BuildKit in GitLab CI aktivieren?
DOCKER_BUILDKIT: "1" im variables-Block. Standard seit Docker Engine 23, Variable sichert ältere Images ab.
4Registry-Cache in GitLab CI?
--cache-from type=registry lädt Layer. --cache-to mode=max schreibt alle Layers zurück. Feature-Branches nutzen Main-Cache.
5Images als Artifacts speichern?
Nein – Images in die Registry pushen. Artifacts nur für kleine Dateien: Reports, Coverage, Scan-Ergebnisse.
6Credentials aus Logs heraushalten?
Als Masked Variable in CI/CD Settings. Nie in .gitlab-ci.yml. Registry-Login mit automatischen $CI_REGISTRY_* Variablen.
7Was ist Inline Caching?
BUILDKIT_INLINE_CACHE=1 bettet Cache-Metadaten ins Image ein. Gepushtes Image als --cache-from nutzbar. Einfacher, aber weniger effizient als mode=max.
8Parallele Jobs ohne Naming-Konflikte?
Image-Tags immer mit $CI_COMMIT_SHORT_SHA oder $CI_JOB_ID versehen. Verhindert Kollisionen paralleler Jobs.
9Trivy in GitLab CI integrieren?
Trivy-Container-Job nach Build. --format gitlab für Security Dashboard. --exit-code 1 blockiert Deploy bei kritischen CVEs.
10Kann kaniko DinD ersetzen?
Für Image-Builds ja, kein privileged nötig. Langsamer, eingeschränkter BuildKit-Support. Sichere Wahl für geteilte K8s-Infrastruktur ohne privileged-Option.