Deploy your Elixir app with a minimal docker container using Alpine Linux and Exrm

Dominic Rubas
May 3, 2016 · 4 min read

tl;dr
Use a second docker container as helper to create a new exrm release of an app in an umbrella project and to deploy it with in a minimal production container running Alpine Linux in an automated way.

Photo by Maziar Behrooz Architecture

You just created your shiny new Elixir app but how do you deploy it now?

Use Docker and Exrm!

But wait. The official Elixir image is almost 300MB! What happened to shipping the smallest container possible?

Alpine Linux to the rescue!

Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.

There are community packages for Erlang and Elixir on Alpine Linux available thanks to Marlus Saraiva, John Regan and Peter Lemenkov. This makes it straightforward to build your own image.

But Marlus also maintains a set of images in different flavours with those packages pre-installed. Let's use those till the official Elixir image switches to Alpine as well.

msaraiva/elixir: Image size: 23.33 MB

Perfect! Let's go and deploy the app…

Are you like me?

$ mix new project -umbrella
  • Do you use Exrm for creating releases?
$ mix release
  • Are you using OS X for your development?

This means you will run into a few issues when you try to automate the deployment.

  • Creating a release under OS X and deploying to a Linux environment gives you a lot of headaches.
  • Only copying the apps/app folder to the docker images, leaves you with no configuration as the apps/app/mix.exs of your app references ../../config/mix.exs.

If you end up with a working phoenix server but cowboy doesn't open a port, it's because `server: true` is not set. Setting `server: true` but not loading the config file has the same effect ;)
Make sure you read the phoenix / exrm guide.

PS: You should consider applying for the Beta for Docker on Mac.

What is the plan?

  1. Create a docker container running Alpine Linux ("Buildhelper")
  2. Copy the whole project into Buildhelper
  3. Select the correct app and build the release inside Buildhelper
  4. Create another docker container running Alpine Linux ("Build")
  5. Copy the release into Build
  6. Configure Build to run the release when started
  7. Ship it!

Of course everything has to be automated and centralised.

Show me!

  • 2 dockerfiles to configure Buildhelper and Build
  • 1 script living in the root of the umbrella project to automate the process. It should take the name of the app as cli argument.

Please use your weapon of choice to create the script and integrate it with your workflow. I'm only going to show the necessary commands to execute the plan.

Save this file as Dockerfile.build in the root folder of the umbrella project.

Save this file as Dockerfile in the root folder of the umbrella project.

Next we create the Buildhelper with the help of Dockerfile.build.

$ docker build -t buildhelper.app -f Dockerfile.build --build-arg APP=app .
  • buildhelper.app is the name the of the Buildhelper docker image
  • app is the name of your app

This creates a docker container with the name buildhelper.app, copies the whole project folder into the container. Then it switches to the app folder under `apps/app`and runs all the necessary steps to create a release.

Now as final step we are creating the Build container with the help of Dockerfile.

$ docker run -v /var/run/docker.sock:/var/run/docker.sock buildhelper.app docker build -t build.app -f Dockerfile --build-arg APP=app --build-arg VERSION=0.0.1 .
  • buildhelper.app is the name the of the Buildhelper docker image
  • build.app is the name of the Build docker image
  • app is the name of your app
  • 0.0.1 is the version of the app. Your script should get that directly from `apps/app/mix.exs`.

For Build we are using the much smaller docker image `msaraiva/elixir`and only copying the release into it. You should end up with an image under 50MB!

Important: You don’t want to run docker inside docker.

With the flag `-v /var/run/docker.sock:/var/run/docker.sock` you give the docker-client inside the docker image access to the docker-daemon of the host. And that is why the last command created the build.app image in OS X and you can run the app now simply with

$ docker run -P build.appUsing /app/releases/0.0.1/app.sh
created directory: '/app/running-config'
Exec: /usr/lib/erlang/erts-7.1/bin/erlexec -noshell -noinput +Bd -boot /app/releases/0.0.1/app -mode embedded -config /app/running-config/sys.config -boot_var ERTS_LIB_DIR /usr/lib/erlang/erts-7.1/../lib -env ERL_LIBS /app/lib -pa /app/lib/app-0.0.1/consolidated -args_file /app/running-config/vm.args -- foreground
Root: /app
12:57:59.556 [info] Running App.Endpoint with Cowboy using http on port 4000

The end?

Customise it to your need and never run your app as root!