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.