Docker-in-Docker in GitLab CI wirklich verstehen
AI generated
CI/CD
.yml
GitLab · CI/CD · Container-Sicherheit
Docker-in-Docker in GitLab CI
wirklich verstehen

Der dind-Service taucht in fast jeder GitLab-CI-Anleitung zum Bauen von Container-Images auf, aber selten wird erklaert, warum er einen privilegierten Runner voraussetzt und welche Risiken das mit sich bringt.

18 Min. Lesezeit docker:dind Privileged Mode Kaniko Buildah

1. Das Grundproblem: Ein Container-Image innerhalb eines Containers bauen

GitLab-CI-Jobs laufen bei den meisten Runner-Konfigurationen selbst innerhalb eines Docker-Containers, der auf Basis des angegebenen image-Werts gestartet wird. Soll dieser Job nun seinerseits ein Docker-Image bauen, etwa das Anwendungsimage fuer den naechsten Deploy-Schritt, stellt sich sofort die Frage, welcher Docker-Daemon diesen Build ausfuehrt. Der Job-Container selbst enthaelt normalerweise keinen laufenden Daemon, nur den docker-Client, und ein Build braucht zwingend einen erreichbaren Daemon, der die Build-Schritte tatsaechlich ausfuehrt.

Docker-in-Docker, kurz dind, loest dieses Henne-Ei-Problem, indem es innerhalb der Job-Umgebung einen zweiten, vollstaendigen Docker-Daemon als eigenen Service startet, gegen den der docker-Client im eigentlichen Job-Container dann seine Befehle richtet. Aus Sicht des Jobs sieht das aus wie ein ganz normaler docker build, tatsaechlich kommuniziert der Client dabei aber ueber das Netzwerk mit einem separaten Daemon-Prozess, der in einem eigenen Service-Container laeuft und speziell fuer diesen einen Job gestartet wurde.

2. Das klassische dind-Setup in der .gitlab-ci.yml

In der Praxis wird dind ueber die services-Direktive eingebunden, die neben dem Haupt-image des Jobs einen zusaetzlichen Container startet und ueber das interne Docker-Netzwerk mit dem Job-Container verbindet. Der Klassiker ist die Kombination aus dem docker-Client-Image fuer den Job und docker:dind als Service, ergaenzt um die Umgebungsvariable DOCKER_HOST, die dem Client mitteilt, wo der Daemon im Service-Container erreichbar ist, da er standardmaessig einen lokalen Socket erwarten wuerde, den es im Job-Container gar nicht gibt.

Ein oft uebersehenes Detail ist die Variable DOCKER_TLS_CERTDIR: Seit neueren dind-Versionen ist TLS zwischen Client und Daemon standardmaessig aktiviert, was zusaetzliche Zertifikatspfade erfordert, die zwischen Job und Service geteilt werden muessen. Wird diese Variable falsch gesetzt oder komplett weggelassen, scheitert die Verbindung meist mit kryptischen TLS-Fehlermeldungen, die auf den ersten Blick nichts mit Zertifikaten zu tun zu haben scheinen.


build_image:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_TLS_VERIFY: "1"
    DOCKER_CERT_PATH: "/certs/client"
  before_script:
    - docker info
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

3. Warum dind zwingend einen privilegierten Runner braucht

Ein Docker-Daemon braucht fuer seine normale Funktion tiefen Zugriff auf Kernel-Funktionen: Er legt Netzwerk-Namespaces an, verwaltet cgroups zur Ressourcenbegrenzung, mountet Overlay-Dateisysteme und manipuliert Netzwerkregeln. All das sind Operationen, die normalerweise root-Rechte und den Zugriff auf Geraetenodes wie /dev voraussetzen, die ein regulaerer, unprivilegierter Container aus Sicherheitsgruenden gerade nicht besitzt. Ein dind-Service, der selbst wieder in einem Container laeuft, braucht daher denselben erweiterten Zugriff wie ein Docker-Daemon auf einem echten Host.

Genau deshalb muss der GitLab-Runner-Executor mit privileged = true konfiguriert sein, damit der Service-Container ueberhaupt in der Lage ist, einen eigenen funktionsfaehigen Daemon zu starten. Diese Einstellung liegt in der config.toml des Runners im Abschnitt runners.docker und ist keine Option, die pro Job in der .gitlab-ci.yml gesetzt werden kann. Ein Runner, der nicht privilegiert laeuft, kann dind-Jobs technisch gar nicht ausfuehren, unabhaengig davon, wie die Pipeline konfiguriert ist.


# Auszug aus /etc/gitlab-runner/config.toml
[[runners]]
  name = "docker-runner"
  executor = "docker"
  [runners.docker]
    image = "docker:27"
    privileged = true
    volumes = ["/certs/client", "/cache"]

4. Sicherheitsimplikationen von privileged: true

Ein privilegierter Container ist praktisch nicht mehr vom Host-System isoliert. Er hat Zugriff auf saemtliche Geraetenodes des Hosts, kann Kernel-Module laden und in vielen Faellen aus dem Container ausbrechen, wenn ein Angreifer beliebigen Code innerhalb dieses Containers ausfuehren kann. Fuer einen GitLab-Runner bedeutet das konkret: Wer beliebigen Code in einer Pipeline ausfuehren kann, etwa ueber eine kompromittierte Abhaengigkeit im Build-Skript, kann unter Umstaenden auf den gesamten Runner-Host zugreifen, nicht nur auf den isolierten Job-Container.

Besonders kritisch wird dieses Risiko bei Shared Runnern, auf denen Pipelines fremder oder weniger vertrauenswuerdiger Projekte laufen. Eine boesartige oder kompromittierte Pipeline eines Projekts koennte theoretisch versuchen, aus dem privilegierten Container auszubrechen und andere Jobs auf demselben Host zu beeinflussen oder Host-Ressourcen abzugreifen. Aus diesem Grund empfehlen sowohl GitLab selbst als auch gaengige Security-Guidelines, privilegierte Runner niemals als Shared Runner fuer beliebige, nicht vertrauenswuerdige Projekte anzubieten, sondern sie auf dedizierte, vertrauenswuerdige Projekte zu beschraenken.

5. Kaniko: Image-Builds ohne Daemon und ohne privilegierten Modus

Kaniko, ein von Google entwickeltes Tool, loest das Grundproblem anders: Statt einen vollstaendigen Docker-Daemon zu emulieren, interpretiert Kaniko das Dockerfile selbst im Userspace und baut das resultierende Image Schicht fuer Schicht direkt im Dateisystem des Containers, ohne dabei auf privilegierte Kernel-Funktionen wie Overlay-Mounts angewiesen zu sein. Dadurch kann Kaniko in einem ganz normalen, unprivilegierten GitLab-CI-Job laufen, was das Sicherheitsrisiko eines privilegierten Runners vollstaendig eliminiert.

Der Kaniko-Executor wird typischerweise direkt als Job-Image verwendet und erwartet Build-Kontext sowie Zielregistry als Kommandozeilenargumente. Ein wichtiger praktischer Unterschied zu docker build ist, dass Kaniko keinen interaktiven Docker-Client-Aufruf simuliert, sondern in einem Rutsch baut und direkt in die Zielregistry pusht, ohne einen separaten docker push-Schritt.


build_image_kaniko:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.23.0-debug
    entrypoint: [""]
  script:
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(printf "%s:%s" "$CI_REGISTRY_USER" "$CI_REGISTRY_PASSWORD" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor
      --context "$CI_PROJECT_DIR"
      --dockerfile "$CI_PROJECT_DIR/Dockerfile"
      --destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

6. Buildah als weitere Alternative mit Rootless-Betrieb

Buildah, urspruenglich aus dem Red-Hat-Container-Oekosystem, verfolgt einen aehnlichen Ansatz wie Kaniko, bietet aber zusaetzlich eine feingranulare Kommandozeile, die einzelne Build-Schritte wie das Erstellen eines neuen Containers, das Ausfuehren von Befehlen darin und das Committen als neues Layer separat steuerbar macht, statt zwingend ein komplettes Dockerfile in einem Rutsch zu interpretieren. Das ist besonders fuer komplexe Build-Pipelines interessant, die dynamisch Layer zusammensetzen wollen, ohne dafuer ein starres Dockerfile zu schreiben.

Buildah kann sowohl mit als auch ohne root-Rechte betrieben werden, wobei der rootless-Modus fuer GitLab-CI-Umgebungen besonders relevant ist, da er komplett ohne privilegierten Container auskommt, sofern der zugrundeliegende Kernel User-Namespaces unterstuetzt. In der Praxis bedeutet das, dass ein regulaerer, unprivilegierter Runner ausreicht, was Buildah zu einer der wenigen Optionen macht, die neben dem reinen Bauen auch interaktivere Layer-Manipulation erlauben, ohne die Sicherheitsnachteile von dind mitzubringen.


# Buildah rootless in einem GitLab-CI-Job (Auszug aus script:)
buildah bud --isolation chroot -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
buildah push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"

7. Performance-Unterschiede zwischen dind, Kaniko und Buildah

In reinen Build-Geschwindigkeitstests liegt dind haeufig leicht vorne, da es den ausgereiften und stark optimierten Docker-Build-Cache-Mechanismus nutzt, der ueber Jahre auf gaengige Dockerfile-Muster hin optimiert wurde. Kaniko implementiert Caching zwar ebenfalls, etwa ueber eine dedizierte Cache-Registry, erreicht aber nicht immer dieselbe Trefferquote wie der native Docker-Layer-Cache, besonders bei komplexeren Multi-Stage-Builds mit vielen Zwischenschritten.

In der Gesamtbetrachtung relativiert sich dieser Geschwindigkeitsvorteil von dind jedoch schnell, wenn man den Aufwand fuer die sichere Bereitstellung privilegierter Runner mit einrechnet: dedizierte, isolierte Runner-Pools, zusaetzliche Netzwerksegmentierung und ein separates Monitoring fuer diese sicherheitskritischen Maschinen. Fuer die meisten Teams ueberwiegt der Sicherheitsgewinn eines rootless-faehigen Tools wie Kaniko oder Buildah den geringen Geschwindigkeitsverlust deutlich, insbesondere wenn Shared Runner im Spiel sind.

8. Wann welches Tool die richtige Wahl ist

Fuer Teams mit voller Kontrolle ueber eigene, isolierte Runner-Infrastruktur und hohem Vertrauen in alle Pipeline-Autoren kann dind weiterhin eine vertretbare Wahl sein, insbesondere wenn bereits viel Tooling und Wissen um den Docker-Client im Team vorhanden ist und komplexe BuildKit-Features wie Multi-Platform-Builds benoetigt werden, die Kaniko nur eingeschraenkt unterstuetzt. Wichtig ist dabei, den privilegierten Runner strikt von Shared-Runner-Pools zu trennen und ausschliesslich fuer vertrauenswuerdige, intern kontrollierte Projekte freizugeben.

Fuer alle anderen Faelle, insbesondere bei Shared Runnern, Multi-Tenant-Umgebungen oder generell dem Wunsch nach einer minimalen Angriffsflaeche, ist Kaniko wegen seiner einfacheren Integration meist die bessere Standardwahl, waehrend Buildah dort punktet, wo mehr Kontrolle ueber einzelne Build-Schritte oder Rootless-Betrieb mit vollem Feature-Umfang eines klassischen Container-Build-Tools gefragt ist.

9. Zusammenfassung im direkten Vergleich

Alle drei Ansätze erreichen dasselbe Ziel, ein Container-Image innerhalb einer GitLab-CI-Pipeline zu bauen, unterscheiden sich aber grundlegend darin, wie viel Kernel-Zugriff sie dafuer benoetigen und welche Sicherheitsabwaegungen damit einhergehen. Die Wahl sollte nie allein nach Geschwindigkeit getroffen werden, sondern immer im Kontext der Vertrauensstellung der Pipeline-Autoren und der Frage, ob der Runner in einer Shared- oder einer dedizierten Umgebung laeuft.

Die folgende Tabelle stellt die drei Optionen entlang der wichtigsten Entscheidungskriterien gegenueber, um die Auswahl fuer das eigene Setup zu erleichtern.

Tool Privilegierter Runner noetig Build-Cache-Qualitaet Empfohlen fuer
docker:dind Ja, zwingend Sehr gut, nativer Docker-Cache Dedizierte, vertrauenswuerdige Runner
Kaniko Nein Gut, ueber Cache-Registry Shared Runner, Multi-Tenant
Buildah (rootless) Nein (bei User-Namespace-Support) Gut, konfigurierbar Feingranulare Layer-Kontrolle
Buildah (root) Nein, aber root-Prozess Gut, konfigurierbar Legacy-Kernel ohne Namespace-Support

Mironsoft

CI/CD-Pipelines, Zero-Downtime-Deployments und Release-Automatisierung

Deployments, die ohne Ausfallzeit und ohne Nervenkitzel laufen?

Wir prüfen bestehende GitLab-Pipelines auf fragile Deployment-Schritte und fehlende Absicherung und bauen daraus einen Release-Prozess mit Zero-Downtime-Deployments, automatisierten Checks und einem Rollback, dem ihr im Ernstfall vertrauen könnt.

Pipeline-Review

Bestehende .gitlab-ci.yml auf Fragilität, fehlende Stages und Sicherheitslücken prüfen.

Zero-Downtime-Deployment

Symlink-Releases, Health-Checks und Rollback-Strategien für Magento-Shops aufbauen.

CI/CD-Automatisierung

Tests, Security-Scans und Deployments zu einer zuverlässigen Pipeline verbinden.

10. Zusammenfassung

Docker-in-Docker: Das Wichtigste auf einen Blick

dind-Prinzip

Ein zweiter Docker-Daemon laeuft als Service-Container neben dem eigentlichen Job.

Privileged Mode

dind braucht Kernel-Zugriff wie ein echter Host, daher zwingend privilegierter Runner.

Kaniko

Baut Images im Userspace ohne Daemon, laeuft in unprivilegierten Jobs.

Buildah

Rootless-faehig mit feingranularer Kontrolle ueber einzelne Build-Schritte.

11. FAQ: Docker-in-Docker: Das Wichtigste auf einen Blick

1Warum reicht der docker-Client im Job-Container nicht allein aus?
Der Client kann Befehle nur an einen laufenden Docker-Daemon weiterleiten, er fuehrt selbst keine Builds aus. Ohne einen erreichbaren Daemon, sei es via dind-Service oder ueber einen extern gemounteten Host-Socket, kann docker build gar nicht funktionieren.
2Ist es sicherer, den Host-Docker-Socket statt dind zu mounten?
Nein, das Mounten von /var/run/docker.sock in den Job-Container ist tendenziell noch riskanter als dind, da der Job dann direkten Zugriff auf den Host-Daemon selbst hat und theoretisch beliebige Container auf dem Host starten oder manipulieren kann.
3Kann ich Kaniko auch fuer Multi-Stage-Dockerfiles nutzen?
Ja, Kaniko unterstuetzt Multi-Stage-Builds vollstaendig, da es das Dockerfile-Format selbst interpretiert. Einzelne sehr neue BuildKit-spezifische Syntax-Erweiterungen werden allerdings nicht immer sofort unterstuetzt.
4Braucht Buildah im rootless-Modus wirklich keine besonderen Runner-Rechte?
Solange der Kernel des Runner-Hosts User-Namespaces unterstuetzt, was bei modernen Linux-Distributionen Standard ist, kann Buildah im rootless-Modus in einem ganz normalen, unprivilegierten Job laufen.
5Was passiert, wenn DOCKER_TLS_CERTDIR falsch gesetzt ist?
Der docker-Client im Job-Container kann dann keine TLS-Verbindung zum dind-Service aufbauen und die Verbindung schlaegt mit einer Zertifikatsfehlermeldung fehl, obwohl das eigentliche Problem eine falsche oder fehlende Variable ist.
6Ist Kaniko langsamer als docker build mit dind?
In vielen Faellen etwas langsamer, insbesondere bei komplexen Multi-Stage-Builds mit vielen Zwischenschichten, da der Docker-native Cache-Mechanismus ausgereifter ist. Der Unterschied ist aber meist gering im Vergleich zum Sicherheitsgewinn.
7Kann ich dind fuer Shared Runner auf GitLab.com nutzen?
Die von GitLab.com bereitgestellten Shared Runner unterstuetzen dind ueber vordefinierte Service-Templates, allerdings mit den generellen Sicherheitseinschraenkungen eines privilegierten Containers, die man bei der Nutzung im Hinterkopf behalten sollte.
8Unterstuetzt Kaniko auch private Registries mit eigenen Zertifikaten?
Ja, ueber die Docker-Config im JSON-Format, die Kaniko im gleichen Format wie ein regulaerer Docker-Client erwartet, koennen auch private Registries mit eigenen Zugangsdaten und Zertifikaten konfiguriert werden.
9Was ist der Hauptunterschied zwischen Kaniko und Buildah in der Praxis?
Kaniko ist auf das reine Bauen aus einem bestehenden Dockerfile fokussiert und einfacher zu integrieren, waehrend Buildah eine feingranulare Kommandozeile fuer einzelne Build-Schritte bietet und sich besser fuer dynamische, skriptgesteuerte Image-Erstellung eignet.
10Muss ich meinen gesamten Runner-Pool privilegiert konfigurieren, wenn ich dind fuer ein Projekt brauche?
Nein, es empfiehlt sich, einen separaten, dedizierten Runner ausschliesslich mit privileged: true fuer die Projekte zu konfigurieren, die dind wirklich benoetigen, waehrend alle anderen Runner unprivilegiert bleiben.