<!--
  canonical-url: https://www.avonture.be/blog/gitlab-docker-out-of-docker
  published:     2025-06-15
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# GitLab - Running Docker-out-of-Docker in your CI

> Learn how to set up Docker-out-of-Docker (DooD) in your GitLab CI/CD pipeline. This guide covers configuring your GitLab Runner to share the host's Docker socket and project files, allowing containers to run `docker run` commands.

---
![GitLab - Running Docker-out-of-Docker in your CI](/img/v2/gitlab.webp)

**TL;DR**

To run Docker commands within a GitLab CI job (Docker-out-of-Docker), you need to configure your GitLab Runner to share the host's Docker socket. This is achieved by modifying the runner's `/etc/gitlab-runner/config.toml` file. In the `[[runners.docker]]` section, you must add `"/var/run/docker.sock:/var/run/docker.sock"` to the `volumes` list. This allows the CI container to communicate with the host's Docker daemon. Additionally, to share project files with nested containers, map the builds directory by adding `"/builds:/builds"` to the volumes as well.

At work, I'm running my CI using Docker. Each time I push changes to GitLab, a CI is running: a Docker container is started by GitLab, pulling my code and running a set of tools.

The container started by GitLab can be based on a PHP, Python, or plain Alpine Docker image. GitLab is then pulling my code inside the container and f.i. run a tool like `phplint` and it's all good.

For a few months now, I've been creating a Docker image to run [Dagger](https://dagger.io) (see [Dagger.io - Using dagger to automate your CI workflows](/blog/dagger-python)). So, here, I run into trouble: my GitLab CI Docker container should be able to run `docker run ... my_dagger_image` and by default, it's not possible.

But I'm in even more trouble when, in my Dagger container, I have to launch another container (one per tool).

Yes, you've guessed it: I'm faced with a situation of running Docker from inside a Docker container already running inside another container.

Let's see how to solve this problem.

Here's the payoff once the runner is configured: a CI job that runs `docker run` — a container starting another container — without the "Cannot connect to the Docker daemon" error you'd otherwise get.

## Why It Works

* The GitLab Runner shares its own Docker socket (`/var/run/docker.sock`) with the CI container, so `docker run` inside the job talks to the *host's* Docker daemon instead of needing its own — no nested Docker-in-Docker engine to install or maintain.
* Sharing the `builds` folder too means a container started from inside the CI job can mount the same project files the job itself was checked out into — without it, paths like `$CI_PROJECT_DIR` would point to a folder invisible to the new container.
* It's two lines added to `config.toml`, not a different CI image or a privileged Docker-in-Docker service — the technique is called **Docker Socket Passthrough**.

As prerequisites, you should have access to the server where your GitLab instance is running (aka the *GitLab Runner*) since you'll need to make changes to the configuration of the runner.

## The runner configuration file

On my GitLab runner server, my current `/etc/gitlab-runner/config.toml` configuration defines Docker as the executor because I have this entry:

**/etc/gitlab-runner/config.toml**

```toml
[[runners]]
  executor = "docker"
```

So, when running my CI, first GitLab will create a Docker container. The default base image can be forced in `/etc/gitlab-runner/config.toml` like below or you just have to specify the base image in your project's `.gitlab-ci.yml` using the `image` tag ([doc](https://docs.gitlab.com/ci/docker/using_docker_images/#define-image-in-the-gitlab-ciyml-file)).

**/etc/gitlab-runner/config.toml**

```toml
[[runners]]
  executor = "docker"

  // highlight-next-line
  [[runners.docker]]
    // highlight-next-line
    image = "php8.4"
```

## Understanding the process

For this part, I'll take a very basic example. For sure, it's not needed to run Docker-in-Docker for a simple "php lint" action. I'll just use that scenario to not over-complicate things.

So, based on the `config.toml` file here above, I can have a `.gitlab-ci.yml` file like the one below:

**.gitlab-ci.yml**

```yaml
phplint:
  script:
    - find . -name "*.php" -print0 | xargs -0 -n1 php -l
```

It'll retrieve all `.php` files and run `php -l <filename.php>` for each inside a Docker container based on PHP 8.4.

Easy and straight-forward.

## Running Docker-out-of-Docker

Based on the same example, we can also use an existing Docker image; like [phpqa/jakzal](https://github.com/jakzal/phpqa) (I've dedicated an article to it: [Docker image that provides static analysis tools for PHP](/blog/php-jakzal-phpqa)):

**.gitlab-ci.yml**

```yaml
phplint:
  image: docker:28.1.1
  variables:
    DOCKER_HOST: unix:///var/run/docker.sock
    DOCKER_TLS_CERTDIR: ""
  script:
    - docker run --rm --volume "$CI_PROJECT_DIR:/app" --workdir /app jakzal/phpqa phplint .
```

Now, to allow this, we'll need to update our GitLab Runner server configuration file:

**/etc/gitlab-runner/config.toml**

```toml
[[runners]]
  executor = "docker"

  [[runners.docker]]
    image = "php8.4"

    // highlight-next-line
    volumes = [ ..., "/var/run/docker.sock:/var/run/docker.sock","/builds:/builds"]
```

> **Caution:**
>
> The syntax `volumes = [ ..., "/var/run/docker.sock:/var/run/docker.sock","/builds:/builds"]` used above is to inform you to append the two values to the list you already have. So, if, right now, you've `volumes = ["/cache", "/certs/client"]` then append values to get `volumes = ["/cache", "/certs/client", "/var/run/docker.sock:/var/run/docker.sock","/builds:/builds"]`

We need to allow the **Docker daemon** which is running on the GitLab runner server to share its socket (the file `/var/run/docker.sock`) so, the GitLab CI Docker container can run `docker run` statements like f.i. `docker run --rm --volume "$CI_PROJECT_DIR:/app" --workdir /app jakzal/phpqa phplint .`

As you can see, there is also a flag `--volume "$CI_PROJECT_DIR:/app"`. This will [share the folder](/blog/docker-volume) referenced by the variable `$CI_PROJECT_DIR` with the container but here, be careful.

Let's say `$CI_PROJECT_DIR` is initialized to `/builds/your-group/your-project`. The `/builds/your-group/your-project` is the one from your Gitlab runner server (so `/builds/your-group/your-project` is a folder on the server). In your GitLab CI Docker container, the `/builds/your-group/your-project` also exists but when you run `docker run --rm --volume "$CI_PROJECT_DIR:/app" --workdir /app jakzal/phpqa phplint .`, you're thus trying to share the host directory with your new container and it'll not work **unless** you've allowed it.

Refer to the update we've already made in the `config.toml` file here above. We've shared `"/var/run/docker.sock:/var/run/docker.sock"` to allow sharing the Docker socket but we've also shared `"/builds:/builds"` and here, it's to share the `builds` folder.

> **Information:**
>
> This technique is called **Docker Socket Passthrough**.

Now, it'll work: we'll share our Docker daemon and also share the folder on the server where our codebase was pulled before running the CI.

From now, we can not only run `docker run [...]` statements but also share our folder with the newly created container.

Why do I need this? Let's take just one example: documentation generation. I'm pushing my documentation as a set of `.md` files (in a folder called `documentation`) then, during my CI, I'm running [Quarto](https://quarto.org/) (see [Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects](/blog/quarto-industrialisation)) to process these Markdown files and based on my configuration (the `_quarto.yml` file), I can generate offline files (like `.docx`, `.pdf`, ...) but also a static HTML site. In this last situation, I'll export the static site somewhere (`./public/documentation`) then publish it using GitLab pages.

## Conclusion

Two lines in `config.toml` — the Docker socket and the `builds` folder — are enough to turn a GitLab CI job into one that can start its own containers, without a nested Docker-in-Docker engine to install or maintain. It's the same trick that makes a [Bruno CLI Docker image](/blog/bruno) or a Dagger pipeline runnable straight from your CI, and it's what I now rely on to run Quarto's documentation build as a container from inside a container.
