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.

