Docker in Docker with Docker Compose#

Docker in Docker is a technology to run docker services inside of a docker container. In this, we’ll take a look at how to set it up with Docker Compose to integrate it with your development environment.

TL;DR

If you’re just interested in the end product, you can find it here.

Networks#

Our goal is going to be to have Docker in Docker (DinD) to be running as a service, and have another service connect to it. First, we need to set up the network so that the two services can actually communicate. Create a file called compose.yaml, and place the following content inside:

networks:
  docker-network:
    name: docker-network
    driver: bridge

This creates a network using the bridge driver. The bridge driver is essentially a software bridge between two (or more) services, that allow the connected services to communicate while providing isolation from services that aren’t connected to the network. The actual creation of the network is handled by Docker itself. You can read more about it here.

Note

If you’ve worked with docker swarm before, you may have heard of services in that context. However, in this article I will refer to services as in the docker compose services.

Volumes#

According to the documentation of the Docker-in-Docker image, we’ll need some volumes to store the TLS certificates. Note that we could skip TLS setup, but it’s actually quite easy and a good thing to do (especially because Docker-in-Docker requires a priviledged service[1]).

We can set up the volumes in by adding the following to our setup:

volumes:
  docker-tls-ca:
  docker-tls-client:

Volumes are a form of anonymous storage with docker. They allow the sharing of data between the host and (one or more) containers. However, they’re completely managed by docker, and as such aren’t meant to be manipulated outside of containers.

Note

If you’re curious, the volumes are stored in /var/lib/docker/volumes/ on Linux.

Services#

Now let’s actually start putting the pieces together, and add our services! Append the following to compose.yaml:

services:
  docker:
    image: docker:27.3-dind
    privileged: true
    volumes:
      - docker-tls-ca:/certs/ca
      - docker-tls-client:/certs/client
    environment:
      DOCKER_TLS_CERTDIR: /certs
    networks:
      - docker-network

  client:
    image: docker:27.3-cli
    volumes:
      - docker-tls-client:/certs/client:ro
    entrypoint:
      - docker
      - version
    depends_on:
      - docker
    networks:
      - docker-network

Wow, that’s a lot! Let’s go through what’s happening. We have two services - one that’s running our docker-in-docker setup, and another that’s acting as our application client. For ease of testing, we’ve made the client just do docker version, which will error if it cannot connect to the docker socket.

Both services are connected to the same network, so that they can communicate with each other. The docker-in-docker service is marked as a privileged service (we haven’t used sysbox). Then, on the docker service, we mounted our volumes to /certs/ca and /certs/client. This means that when the docker service creates the certificates, it will also be created somewhere on your machine. The client certificates (docker-tls-client) are then mounted to /certs/client on the client service, so that the client can authenticate itself. The :ro at the end marks the docker-tls-client volume as read-only on the client service.

Environment Variables#

However, if we run it in it’s current state, it still will not be able to connect to the docker in docker service, because it’s still looking for a /var/run/docker.sock instead of the tcp:// socket the Docker-in-Docker image creates. You’ll need to add the following to the client service:

    networks:
      - docker-network
    # NEW!!
    environment:
      DOCKER_TLS_CERTDIR: /certs
      DOCKER_HOST: tcp://docker:2376
      DOCKER_TLS_VERIFY: "1"
      DOCKER_CERT_PATH: /certs/client

This tells docker where the socket is, and how to use TLS.

Tip

If you named your Docker-in-Docker service something other than docker, you’ll need to change it’s hostname to something and change the DOCKER_HOST to tcp//$HOSTNAME:2376.

Finally, we can run

docker compose build
docker compose up -d
docker compose logs client

To see that the client was able to access docker, without needing to access the host system!

Quick Personal Note#

While researching, I found almost zero reference compose files for setting up this kind of thing (maybe Docker-in-Docker is a bit niche)! As a result, once I figured it out, I decided to make a sample repository so that others would hopefully not spend as much time as I did hunting down environment variables :)

One thing I haven’t (yet) figured out how to do is getting the docker service to also start a Docker Swarm upon startup - for some reason, that breaks the setup on my machine. If anyone figures it out, please file an issue and I’ll update this blog post.