Docker-in-Docker (DinD) vs Docker-out-of-Docker (DooD): A Practical CI/CD Runner Setup Guide

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

If you’re setting up a CI/CD pipeline that needs to build Docker images inside a container runner, you’ll inevitably run into the question: DinD or DooD? It took me a fair amount of research before I truly understood the difference — this article summarizes everything so you don’t have to go through the same rabbit hole I did.

Up and Running in 5 Minutes — DooD on GitLab Runner

If you need results fast, here’s how to set up DooD (Docker-out-of-Docker) for GitLab Runner — this is what I’m running in production and it’s the most stable approach:

Step 1: Install GitLab Runner with Docker Executor

# Install GitLab Runner
curl -L --output /usr/local/bin/gitlab-runner \
  https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
chmod +x /usr/local/bin/gitlab-runner

# Register the runner — choose executor: docker
gitlab-runner register \
  --url https://gitlab.com \
  --registration-token YOUR_TOKEN \
  --executor docker \
  --docker-image docker:24 \
  --docker-volumes /var/run/docker.sock:/var/run/docker.sock

Step 2: .gitlab-ci.yml File for Building Images

build-image:
  image: docker:24
  services: []   # No dind service needed when using DooD
  variables:
    DOCKER_HOST: unix:///var/run/docker.sock
  script:
    - docker build -t myapp:$CI_COMMIT_SHORT_SHA .
    - docker push myapp:$CI_COMMIT_SHORT_SHA

That’s it. The runner can now build Docker images by communicating directly with the host’s Docker daemon. Simpler than you’d expect.

DinD vs DooD — What’s Actually Different?

The two names look similar but the underlying mechanisms are completely different. Understanding them correctly means choosing correctly — and when your pipeline suddenly breaks, you’ll know exactly where to debug.

Docker-in-Docker (DinD)

DinD means running a Docker daemon inside the container. The runner container starts its own daemon, completely independent from the host.

# .gitlab-ci.yml using DinD
build-dind:
  image: docker:24
  services:
    - docker:24-dind          # Separate daemon running inside the service container
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_CERT_PATH: "/certs/client"
    DOCKER_TLS_VERIFY: 1
    DOCKER_HOST: tcp://docker:2376
  script:
    - docker build -t myapp:latest .
    - docker push myapp:latest

DinD requires running the container with the --privileged flag. This flag gives the container access to kernel subsystems — namespaces, cgroups, overlay filesystem — allowing it to spin up another Docker daemon inside.

Docker-out-of-Docker (DooD)

DooD doesn’t create a new daemon. Instead, the runner container mounts the host’s Docker daemon socket (/var/run/docker.sock) into the container. Every docker command inside the container is actually talking to the daemon on the host.

# Run a container with DooD — mount the host socket
docker run -v /var/run/docker.sock:/var/run/docker.sock \
  -it docker:24 sh

# Inside this container, the following command runs a container ON THE HOST
docker run hello-world

Quick Comparison

  • DinD: Separate daemon, better isolation, requires --privileged, slower (must re-pull images from scratch each time)
  • DooD: Uses the host daemon, faster (shared image cache), simpler, but containers have host-level Docker privileges

Deep Dive — Why Does DinD Need –privileged?

The Docker daemon needs access to kernel subsystems like namespaces, cgroups, and overlay filesystem. When running inside a container, these permissions are restricted by default. The --privileged flag removes most of those restrictions.

This is also DinD’s biggest weakness. A container running with --privileged can escape to the host if exploited — many published container escape CVEs involve this flag. That’s why security teams at many companies outright block --privileged on production clusters.

Safer DinD Configuration with TLS

# docker-compose.yml for a self-hosted DinD runner
version: '3.8'
services:
  dind:
    image: docker:24-dind
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
    volumes:
      - dind-certs-ca:/certs/ca
      - dind-certs-client:/certs/client
      - dind-storage:/var/lib/docker
    networks:
      - ci-network

  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    volumes:
      - ./config:/etc/gitlab-runner
      - dind-certs-client:/certs/client:ro
    environment:
      DOCKER_HOST: tcp://dind:2376
      DOCKER_TLS_VERIFY: 1
      DOCKER_CERT_PATH: /certs/client
    networks:
      - ci-network
    depends_on:
      - dind

volumes:
  dind-certs-ca:
  dind-certs-client:
  dind-storage:

networks:
  ci-network:

With this configuration, the DinD daemon communicates over TLS — port 2375 (unencrypted) is not exposed.

Advanced — Kaniko: Building Images Without a Docker Daemon

Kaniko was the solution I found after a security review rejected --privileged on our Kubernetes cluster. Instead of arguing, I switched to Kaniko — and it solved the problem immediately. Kaniko builds Docker images from a Dockerfile without needing a daemon, runs entirely in user space, and requires no special privileges whatsoever.

# .gitlab-ci.yml with Kaniko
build-kaniko:
  image:
    name: gcr.io/kaniko-project/executor:v1.21.0-debug
    entrypoint: [""]
  script:
    - /kaniko/executor
      --context $CI_PROJECT_DIR
      --dockerfile $CI_PROJECT_DIR/Dockerfile
      --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
      --cache=true
      --cache-repo $CI_REGISTRY_IMAGE/cache

Kaniko build times are roughly 20-30% slower than DooD because there’s no daemon layer cache available. That trade-off is usually acceptable when security is a higher priority than speed.

GitHub Actions — DooD Built-In

GitHub-hosted runners come with Docker pre-installed, so you can just use it directly:

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Push image
        run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

For self-hosted GitHub Actions runners, the setup is similar to GitLab DooD — just mount the socket into the runner container and you’re done.

Production Tips from Real-World Experience

Effective Layer Caching with DooD

DooD’s biggest advantage is sharing the image cache with the host. But after a few weeks of continuous pipeline runs, that cache can balloon to 50-80 GB — I’ve seen runner VPSes run out of disk because of this. Add a periodic cleanup cronjob:

# Clean up Docker cache weekly — add to crontab
0 3 * * 0 docker system prune -f --filter "until=168h"

Setting Socket Permissions Correctly

# Check the group of the Docker socket
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker ...

# The runner user must belong to the docker group
usermod -aG docker gitlab-runner

# Or fix this in the runner image's Dockerfile
RUN groupmod -g 999 docker && usermod -aG docker runner-user

Real-World Results

On a production cluster running 30+ containers, switching from DinD to DooD reduced resource usage by ~40%. The main reason: no need to spin up an additional daemon in each runner container. On top of that, the image cache is shared across jobs — instead of every job re-pulling images from scratch like with DinD.

When to Use Which?

  • DooD: Self-hosted runners on VMs/VPS where performance matters and a small team controls the host
  • DinD: When you need isolation between pipelines, shared environments, or testing complex Docker networking
  • Kaniko: Kubernetes clusters, security-sensitive environments, or anywhere you can’t use --privileged

I use DooD for GitLab runners on a dedicated VPS — it’s fast, simple, and there’s no daemon overhead to deal with. For Kubernetes clusters shared with other teams, Kaniko is the safer choice. In the end, it’s not about which tool is “best” — it’s about choosing the right tool for your trust level and isolation requirements.

Share: