DEV Community

bakenator
bakenator

Posted on

Deploying Elixir and Docker in Docker

Recently I have been building a CI (continuous integration) app in elixir.

Why would I do that when there are plenty of good SaaS options? For Fun!

Along the way I have found out about a few interesting things that I thought I could share. This post will focus solely on Elixir Release and Docker in Docker. I will post another article on the elixir app itself.

What Does the CI App Do?

This is a simple CI application. It waits for Github webhook notifications, downloads and tests the new code, then sends the results back to Github.

Working with github is detailed here: https://developer.github.com/v3/guides/building-a-ci-server/

How Does it Work?

Here is where things start getting tricky. Because we want to isolate the code being tested, we will need to run the tests inside a docker container.

But also for portability of the CI app, our elixir code will also need to be running in a docker container.

This brings us to the ultimate mind bender: Docker in Docker!

Lets jump right in.

Dockerizing an Elixir Application

Best practice to send elixir apps off to production is to use the built in Erlang Releases concept. For elixir apps, this can be easily done through the Distillery library. https://elixirschool.com/en/lessons/libraries/distillery/

One hiccup is that the release needs to be built on an operating system matching the operating system where it will be deployed. Docker can come to the rescue here because it allows you to build your elixir releases inside of docker to guarantee the deployed OS is identical to the build OS.

A good post on the subject is here: https://medium.com/@pentacent/getting-started-with-elixir-docker-982e2a16213c
And the officially recommended Dockerfile example is here: https://hexdocs.pm/distillery/guides/working_with_docker.html

Docker in Elixir in Docker

Remember, we want the OS with our elixir app to include docker so that we can launch containers for testing. The guides above show how to build an elixir release in docker, but not with the OS needed for docker in docker.

That OS is called "Docker in Docker"!

The official image is docker:dind, but there are several variants on dockerhub. We will be using a specific version docker:18.09.0-dind so that we are sure it matches the elixir:1.7.2-alpine OS used to build the release

From the hexdocs.pm example, make a new Dockerfile and change the line
FROM alpine:${ALPINE_VERSION}
to
FROM docker:18.09.0-dind

Dockerfile CMD Bug

Through trial and error I found that the line
CMD trap 'exit' INT; /opt/app/bin/${APP_NAME} foreground
was overriding the CMD from the docker:18.09.0-dind image.
This results in the docker daemon not starting, which makes the image useless.

To fix this I tracked down what the docker:18.09.0-dind CMD is and put it in a shell script.

So in the top level project directory add the following file start.sh
The sleep command is there to allow the docker daemon to start up before the elixir app starts.

#!/bin/sh
dockerd-entrypoint.sh & 
sleep 10
/opt/app/bin/ci foreground

Then update the end of the DockerFile to

COPY ./start.sh .
RUN ["chmod", "+x", "start.sh"]
ENTRYPOINT /start.sh

Inner Docker Image

We've sorted out the container that runs the elixir app on a docker:dind image. Now lets prepare the docker image that the tests will use to run.

Create a folder at test_runner
Within this folder my Dockerfile looks like this

FROM node:11.12.0-alpine
RUN apk update && apk upgrade && apk add --no-cache bash git openssh
COPY . .
RUN ["chmod", "+x", "start.sh"]
ENTRYPOINT /start.sh

and the start.sh file looks like this (with personal details replaced)

#!/bin/sh
git clone --depth 1 -b branch_name https://$TOKEN@github.com/$USER_NAME/$PROJECT.git
cd $PROJECT_NAME
npm install
npm run test

Your inner docker image can be whatever you want. And note it doesn't need any special docker:dind OS to run.

Getting the Image into Your App

So we've made the testing image, but now we need the elixir app to have access to it inside the docker:dind system.

We can do this using the docker save and docker load commands. These allow us to save a docker image to a file and then load it back up.

If we run docker save test_runner > test_runner.tar in the test_runner folder, we will get a new test_runner.tar file.

Then in our main ./Dockerfile we can add the line (right before ENTRYPOINT)
COPY ./test_runner/test_runner.tar .
This brings the file into our Elixir image.

Finally we add this line to the main ./start.sh file
docker load < ./test_runner.tar
This adds the image to our docker application so that is can be fired up from the elixir app.

The final ./start.sh file should look like this

#!/bin/sh
dockerd-entrypoint.sh & 
sleep 10
docker load < ./test_runner.tar
/opt/app/bin/ci foreground

Starting a Container in Elixir

Now that we have set up all the infrastructure for our apps, we are ready to use the container in the elixir app.

Wherever it is needed you can use
System.cmd("docker", ["run", "test_runner"])
That will run the docker image and return you the output and exit status in a tuple.

Don't forget that we can tailor the test runner on the fly by passing in environment variables as well.

Conclusion

Thank you if you've made it this far. No one said docker in docker would be easy!

I realize that elixir deployment methods can be contentious, and docker in docker even more so. I welcome any comments on alternative/better systems we could use here instead.

Now here is a build script I wrote to automate all of the build steps after you set up your elixir project, Dockerfiles, and start.sh scripts.

Final note, you must use the --privileged flag to make use of docker in docker.

#!/bin/sh
cd test_runner
docker build -t test_runner .
docker save test_runner > test_runner.tar
docker image rm test_runner
cd ../
docker build -t elixir_app .
rm test_runner/test_runner.tar
docker run -p $OUTER_PORT:$INNER_PORT --privileged -d elixir_app

Top comments (0)