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.
Inhaltsverzeichnis
- 1. Das Grundproblem: Ein Container-Image innerhalb eines Containers bauen
- 2. Das klassische dind-Setup in der .gitlab-ci.yml
- 3. Warum dind zwingend einen privilegierten Runner braucht
- 4. Sicherheitsimplikationen von privileged: true
- 5. Kaniko: Image-Builds ohne Daemon und ohne privilegierten Modus
- 6. Buildah als weitere Alternative mit Rootless-Betrieb
- 7. Performance-Unterschiede zwischen dind, Kaniko und Buildah
- 8. Wann welches Tool die richtige Wahl ist
- 9. Zusammenfassung im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.