Docker in Docker in Jenkins pipeline

I had a simple case:

  • a build which creates some database Docker container so that integration tests can run against it - using Docker Compose for example
  • I want to run the build in a Docker container so that my build environment can be defined using a Dockerfile stored together with the source
  • I want to use the Jenkins pipeline and use docker.build(...).inside {} to run my steps in the Docker container defined by the Dockerfile above
  • of course, everything runs on a generic agent enabled for Docker

The build environment

First thing first, in order for my build container to run Docker & Docker Compose, the Dockerfile file must install them. They won't be available by magic :)

# Installing Docker
RUN wget https://get.docker.com/builds/Linux/x86_64/docker-1.11.0.tgz -O docker.tgz
RUN tar -xvzf docker.tgz
RUN mv docker/* /usr/bin/
RUN chmod +x /usr/bin/docker

# Installs Docker Compose
RUN curl --fail --silent -L https://github.com/docker/compose/releases/download/1.6.2/docker-compose-`uname -s`-`uname -m` > /usr/bin/docker-compose
RUN chmod +x /usr/bin/docker-compose

Talking to Docker

Now, inside the Jenkinsfile, I need to connect my build container to the outer Docker instance. This is done by mounting the Docker socket itself:

docker.build('my-build-image').inside("--volume=/var/run/docker.sock:/var/run/docker.sock") {
   // The build here
}

So far so good.

Now, when I run the actual build steps, I must somehow be able to pass the exact address of the host, so that my integration tests know where to find the database. Since I'm using Gradle, it should look like:

./gradlew -PmyIntegrationDockerHost=???

In other words, I actually need to pass to my build, running in the build container, the address of the Docker host so that it can connect to the database container it has created. Still following me?

If I have this address, let's say in a hostIP variable, I can just change the Jenkinsfile to use the --add-host option and do:

docker.build('my-build-image').inside("--volume=/var/run/docker.sock:/var/run/docker.sock --add-host dockerhost:${hostIP}") {
   sh './gradlew ... -PmyIntegrationDockerHost=dockerhost'
}

Getting the IP

In the end, I just need to get the host IP from within the Jenkinsfile.

I can do this using a complex sequence of shell and property injection:

sh '''\
      HOSTIP=`ip -4 addr show docker0 | grep 'inet ' | awk '{print $2}' | awk -F '/' '{print $1}'`
      '''

   def props = readProperties(file: 'host.properties')
   String hostIP = props.HOSTIP

Using Groovy only?

Now, I'd love to do this using Groovy and the pipeline DSL only... I imagined I could just do:

String hostIP = InetAddress.localHost.hostAddress

(this call must be approved by going to Manage Jenkins > In-process Script Approval)

but this does return only 127.0.0.1, which is not very useful in our case.

Conclusion

The shell approach is a bit verbose but does its job. Complete source code, using a simplified build to test the connection to a Postgres container, is available at https://github.com/nemerosa/jenkins-docker.

In the end, it'd be nice if the pipeline DSL could just provide this as an extra parameter on the inside method.