Getting Started With Elixir & Docker

Learn how to build Docker containers for Elixir apps with this guide.

Philipp
Philipp
Nov 15, 2017 · 7 min read
Lots of containers. (Original photograph by Chuttersnap/StockSnap. License: CC0)

A Quick Recap on Docker & Elixir Releases

Why use Docker with Elixir?

A Release can only run on systems sufficiently similar to the build system. What does that mean? Unfortunately, that question doesn’t have a trivial answer. Most importantly, operating system and processor architecture need to match and system libraries need to be compatible. The former is why you can’t run a Release that was built on a macOS machine on Linux. The latter is why a Release that you build with Ubuntu 17.10 doesn’t work on Ubuntu 14.04: Their C standard libraries (specifically glibc) are not ABI-compatible. This means that your application might work perfectly fine on one system, but crash with an obscure error message on another.

Fortunately, there is a solution to this problem and it’s containers.

Step-by-Step Guide

Installing Docker

There are multiple versions of Docker available but for our purposes Docker Community Edition (Docker CE) is more than sufficient. The Docker website has installation instructions for Windows, macOS and Linux.

If you are using Linux, you might already have Docker installed but many distributions ship with outdated versions. So I recommend installing the latest version from the Docker website to make sure there are no compatibility issues.

What’s in a Dockerfile?

A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
- Docker reference documentation

Here are the most important commands that we’ll be using to create our image:

  • FROM initializes a new image based on an existing image. Alternatively, FROM scratch can be used.
  • COPY copies files from the build context (which is usually the directory in which the Dockerfile resides) into the image.
  • ENV sets persistent environment variables.
  • RUN executes one or more shell commands.

Each command in a Dockerfile creates a new layer of the final image. I don’t want to go into the internals of Docker too much but for our purpose, it is desirable to keep the number of total layers as low as possible. If you are interested in how exactly layers work, I find that this short article gives a better explanation than the official Docker documentation

Dockerfile Stages

Elixir, on the other hand, is a compiled language and always requires a dedicated build phase. During the build phase you need files and programs that are not necessary in order to run the application: Mix, Git and your source code all don’t need to be available on your production system.

This is where Docker’s multi-stage builds come in handy: You can set up environments for building your application that are not going to be part of your final deployment container.

How do multi-stage builds work? Easy: Every FROM command in your Dockerfile initializes a new stage. Later stages can copy files from previous stages. The last stage becomes your final image while all other stages are discarded.

Ready, Set …

git clone https://github.com/wmnnd/elixir-docker-guide
cd elixir-docker-guide

The Build Stage

The first step is to pick an existing image from Docker Hub for the build phase: Let’s go with Paul Schoenfelders’s awesome alpine-elixir image which ships with everything we need to compile an Elixir application.

FROM bitwalker/alpine-elixir:1.5 as build

Next, let’s copy our source code into the Docker container:

COPY . .

If you want to be more selective about what you copy from your source folder, you could also do something like this instead:

COPY rel ./rel
COPY config ./config
COPY lib ./lib
COPY priv ./priv
COPY mix.exs .
COPY mix.lock .

Being more explicit with the folders you want to copy, can be useful if you want to avoid issues with artifacts in the _build folder or previously downloaded dependencies.

Next, we want to fetch the application’s dependencies, compile it and build a Release with Distillery:

RUN export MIX_ENV=prod && \
rm -Rf _build && \
mix deps.get && \
mix release

The rm -Rf _build bit is not necessary if you are only building your image in an isolated CI environment or if you have chosen the more explicit way of copying your files mentioned above.

At this point, you can already try building an image from your Dockerfile. Call the following command and you should see the application being built by Mix and Distillery — inside a Docker container:

docker build -t elixir-docker-guide .
Building our application in a Docker container. We can’t use the container yet but it’s working so far!

The -t parameter for docker build gives a name or tag to the newly created image. Like this, we can access it at a later point. We also need to specify the build context, i. e. the local folder from which to build the image. Since this is the current folder, we simply put . there.

Finishing the Build Stage

RUN APP_NAME="clock" && \
RELEASE_DIR=`ls -d _build/prod/rel/$APP_NAME/releases/*/` && \
mkdir /export && \
tar -xf "$RELEASE_DIR/$APP_NAME.tar.gz" -C /export

While this snippet might seem a bit obscure at first, it’s actually pretty simple: It extracts the .tar.gz archive created by Distillery into a folder called /export .

The Deployment Stage

FROM pentacent/alpine-erlang-base:latest

The next step is to copy the compiled Release from the previous stage:

COPY --from=build /export/ .

It’s good practice to use a non-root user, even inside of a container. This is why we make Docker switch to the default user (which has already been created in alpine-erlang-base).

USER default

Finally, all we need to do is specify an ENTRYPOINT and a CMD for our image. ENTRYPOINT defines the application that is started when creating a container from our image; CMD specifies the default arguments for that application:

ENTRYPOINT ["/opt/app/bin/clock"]
CMD ["foreground"]

Let’s give it a try! Build and run the application:

docker build -t elixir-docker-guide .
docker run -t elixir-docker-guide

That wasn’t too hard, was it?

The Full Recipe

You can use this as a template for your own projects. License: CC0.

From Here On Out

Phoenix

ENV MIX_ENV=prod
RUN apk update && \
apk add -u musl musl-dev musl-utils nodejs-npm build-base
RUN mix deps.get
RUN mix compile
RUN cd assets && \
npm install && \
node ./node_modules/brunch/bin/brunch b -p && \
cd .. && \
mix phx.digest
RUN mix release

If you are using a more elaborate asset pipeline or if you want to take better advantage of Docker’s caching capabilities, you can even make this a dedicated Docker stage.

Umbrella Applications

What About the Base Image?


I hope you enjoyed this guide! If you have any questions or suggestions, please let me know!

This article is part of an ongoing series about developing and deploying Elixir applications to production. Make sure to sign up for my email list if you enjoyed this article.

Get early access to my upcoming ebook on deploying Elixir apps. No spam, I promise :-)

Credit Where Credit is Due

Philipp

Written by

Philipp

I make software. Passionate about Elixir. User of C++, Ruby & JavaScript. Current project: https://www.dblsqd.com