Learn GitLab CI/CD - Docker Integration (Docker-in-Docker & Kaniko)
Episode 6 of 21

Learn GitLab CI/CD - Docker Integration (Docker-in-Docker & Kaniko)

This episode dissects Docker integration in GitLab CI/CD: running jobs with custom images, the image build technique via Docker-in-Docker (DinD), then Kaniko which is more secure without a privileged daemon, all the way to automatically building and pushing images to the GitLab Container Registry.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 5 you learned to manage variables and secrets in GitLab CI — using predefined variables, hiding values with masks, and fetching secrets from Vault. Now let's level up: almost every modern application is packaged as a Docker image before being shipped to environments. The question is: where is that image built? It could be on each developer's laptop, but that's prone to "works on my machine". The correct solution is to build and push images inside the GitLab pipeline — from one source code, one result.

The catch is that building an image inside CI isn't as naive as running docker build on your laptop. CI containers don't have a Docker daemon by default, and there are several techniques with different security tradeoffs. This episode dissects the two most popular techniques — Docker-in-Docker (DinD) and Kaniko — complete with their integration into the GitLab Container Registry, GitLab's built-in registry that updates every time you push.

Main Discussion

Running Jobs with Custom Docker Images

Before building images, recall: with the Docker executor, every job runs inside a container. The image: keyword determines which container is used. Each job can use a different image depending on its needs:

Choosing an image per job
build_js:
  image: node:20-alpine
  script:
    - node --version
    - npm ci
    - npm run build
 
test_py:
  image: python:3.11
  script:
    - python --version
    - pip install -r requirements.txt
    - pytest

Here the build_js job runs inside the node:20-alpine container — small, lightweight, and already containing Node.js — while test_py uses python:3.11 to run pytest. The important principle: pin image versions. node:latest can change its contents at any time and break your pipeline out of nowhere. Use specific tags — node:20-alpine or even a digest — so your builds are reproducible.

Tip

Alpine-based images are far smaller than Debian variants (hundreds of MB vs tens of MB). For CI jobs created and destroyed on every run, a small image means faster startup and cheaper runner quotas. Choose the Alpine variant when the tools you need are available there.

Building Images Inside the Pipeline

Running jobs with custom images is how you use Docker. Now comes the turn to build your own images. The goal is clear: configure once in .gitlab-ci.yml, and every team gets exactly the same image, built from exactly the same commit. No more "let's try building it on my machine first".

There are two dominant techniques, and both are equally valid — they only differ in infrastructure requirements and risk profile.

Technique 1: Docker-in-Docker (DinD)

DinD works by "tucking" a Docker daemon inside the job container. Jobs build images with docker build, exactly like on your laptop, but what executes it is the daemon inside the docker:dind service container:

Build with Docker-in-Docker (DinD)
build_image:
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  tags:
    - docker

Its advantage: the docker build, docker login, and docker push commands work as-is — familiar, fully supported, and fast for first builds because a full daemon is ready inside. But there's a price to pay: DinD requires the runner to be in privileged mode.

Warning

Privileged mode means the job container has full access to the runner host's kernel. If the DinD daemon is compromised, an attacker can take over the runner machine itself. That's why DinD should only run on dedicated runners you control yourself — never put it on a shared runner owned by another team. For most teams, the risk isn't worth the convenience.

Kaniko is Google's image build tool that works without a daemon at all. It executes Dockerfile instructions directly as a user-space process inside the container — so it doesn't need privileged mode. This makes it safe to run even on shared runners, and it's GitLab's official recommendation for building images.

Build with Kaniko
build_image:
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - kaniko --context $CI_PROJECT_DIR \
        --dockerfile $CI_PROJECT_DIR/Dockerfile \
        --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  tags:
    - docker

Two important details in the config above. First, entrypoint: [""] is overridden because the Kaniko image has a built-in entrypoint that runs the executor directly — without that override, your script won't be executed. Second, GitLab Runner automatically injects the GitLab registry credentials into the Kaniko container, so the image can be pushed straight to the GitLab Container Registry without a manual docker login — this is why the official GitLab template looks so simple.

Kaniko also supports layer caching via the --cache=true flag with --cache-repo to store intermediate layers in the registry — a concept we'll dive into in episode 8 on caching.

DinD vs Kaniko Comparison

AspectDinDKaniko
Runner requirementMust be privilegedNo privileged needed
ArchitectureDocker daemon inside a containerUser-space process without a daemon
SecurityHigh risk if compromisedSafe on shared runners
Commands useddocker build + manual pushSingle kaniko command
First buildFast (full daemon)Slightly slower
Layer cachingNeeds BuildKit / --cache-from--cache=true + --cache-repo

Tip

Rule of thumb: if your runner is your own and dedicated, DinD is convenient. If the pipeline can run on shared, group, or SaaS runners — choose Kaniko. For teams just starting out, go straight to Kaniko: better security and a shorter config.

Integration with the GitLab Container Registry

Every GitLab project has a built-in Container Registry — collaborators don't need to create their own registry. The predefined variables you should know: $CI_REGISTRY_IMAGE (the project image path, e.g. registry.gitlab.com/team/my-app), $CI_REGISTRY_USER, and $CI_REGISTRY_PASSWORD for authentication.

A common industry pattern: tag images with the commit SHA so every commit has a unique, traceable image, then add extra tags for convenience:

Push an image with multiple tags at once
kaniko --context $CI_PROJECT_DIR \
  --dockerfile $CI_PROJECT_DIR/Dockerfile \
  --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
  --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH \
  --destination $CI_REGISTRY_IMAGE:latest

Once the image is in the registry, the next stage can use it directly as image: in a job:

Using a built image in the next stage
deploy:
  stage: deploy
  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  script:
    - docker run --rm $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA test

Notice how $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA links the entire pipeline: the build_image job builds and pushes, the deploy job pulls the exact same image from the registry. No stale images, no version guessing — everything is wired together by automatically populated GitLab variables.

Note

For production pipelines, don't tag latest from every commit push — it gets overwritten continuously and loses its meaning. Let latest follow the main branch only, for example through a rules condition that only runs that step when a commit lands on main. We already covered rules configuration in episode 4.

Closing

In this episode 6, you've learned the containerization foundations of GitLab CI/CD:

  • image: determines which container a job uses, and image versions must be pinned for reproducible pipelines.
  • Docker-in-Docker (DinD) builds images with a daemon inside the container, fast and familiar, but requires a privileged runner which is a security risk.
  • Kaniko builds images without a daemon and without privileged mode — GitLab's primary recommendation for secure image builds.
  • GitLab Container Registry is fully integrated: credentials are injected automatically, and the $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA tag links build and deploy traceably.

The "build the image in CI, push to the registry, then use it in the next stage" pattern is the backbone of enterprise pipelines. But building images is only part one — other build outputs (binaries, zips, reports) also need to move between jobs in a controlled way, and sometimes you'll want to publish packages for other teams to use. In episode 7 we'll cover artifact management — how to store, transfer, and download build outputs between jobs — along with GitLab Package Registry integration for publishing npm, Maven, PyPI, Go, and Helm chart packages. See you in episode 7!

Learn GitLab CI/CD - Docker Integration (Docker-in-Docker & Kaniko) | Learn GitLab CI/CD