Deploying Elixir applications with Docker

or How to set up a Continuous Delivery pipeline for Elixir

Elixir has been gaining a huge popularity among developers in the past couple of years, pushing it to the 7th most loved language according to StackOverflow. Leveraging the battle-tested Erlang VM, this modern and functional programming language is designed for building highly-performant applications running across multiple cores.

There are plenty of resources to help you learn Elixir, but perhaps, one of the most uncovered (yet not strongly opinionated) topics in the community is how to deploy Elixir applications. Hence, in this post, we want to introduce you the tools used to deploy Elixir applications with Docker and how you can set up an automated Continuous Delivery (CD) pipeline.

Building releases

We’ll be using a basic Phoenix application, which you can find on GitHub, however things should be pretty similar for any other Elixir app. The simplest way to run it is to install dependencies with mix deps.get and execute mix phx.server . That’s great while developing, however the most common approach to deploy Elixir applications is to build a release package.

The release is a precompiled, stripped down and compressed package with everything needed to run in production. So, you don’t need to worry about dependencies and you can deploy it anywhere, as long as the target operating system and architecture match the machine where the release was compiled. Isn’t it handy?!

There are a number of projects available for packaging and releasing Elixir applications, such as Exrm and Edeliver. Exrm has been replaced by Distillery and is the preferred way to release Elixir applications today, so that’s the tool we’ll be using here.

To set it up, just add distillery to your mix.exs and run mix deps.get . You’ll need to have Elixir 1.3+ and Erlang 18+ installed for Distillery to work.

defp deps do

[{:distillery, "~> 1.5", runtime: false}]

end

Now that we have it installed, we’ll need to configure the release. By simply running mix release.init , Distillery generates the release configuration file rel/config.exs that defines environments, whether to include system libraries and what version to use.

Finally, to build the release all we need is to run mix release and we should find dockbit_phoenix_example.tar.gz created under _build/dev/rel/dockbit_phoenix_example/releases/0.0.1 .

Notice though, the default release environment is dev which is good for development, but really we’re building a deployment pipeline for production, aren’t we?

To build a production release for our example Phoenix app, we’ve introduced config/prod.secret.exs that pulls database configuration from environment variables (see it in this GitHub commit). It’s a good practice to store configs in the environment variables, aka The Twelve-Factor App. Also, be aware that environment variables need to be present during compile time, as described in the excellent post by the folks at Plataformatec.

So, now we can finally build a production release with MIX_ENV=prod mix release . Cool, we have the production release package under _build/prod/rel/dockbit_phoenix_example/releases/0.0.1 and ready to move on.

Storing releases

In order to deploy a release, inside a Docker container or in any other server environment, we would need to download it from artifcat repository. There are a number of options available: Github releases, AWS S3, or hosting them ourselves.

Since AWS provides a cheap and fast storage with the tooling required to manipulate files, we will be using it with their provided AWS CLI.

Assuming you have the AWS CLI properly configured, to upload an Elixir release we can use an aws s3 cp command:

aws s3 cp --acl public-read _build/prod/rel/dockbit_phoenix_example/releases/0.0.1/dockbit_phoenix_example.tar.gz s3://dockbitexamples-elixir-phoenix/releases/0.0.1.tar.gz

We’ve created a separate dockbitexamples-elixir-phoenix S3 bucket for our releases to be able to fetch them easily with a curl later. Note, we’ve set it to public-read for the sake of simplicity — ideally, you would keep them private and configure AWS credentials as environment variables.

Running inside Docker

As we have our releases stored in the cloud, we could download them and run on any server, without needing to install any Erlang/Elixir dependencies. The package is self-contained and ready to be deployed.

To make things ready for a scalable production environment with Kubernetes or Docker Swarm, we will build a Docker image and push it to Docker Hub. Luckily, Docker makes it really easy as we only need a Dockerfile :

Lines 6-12: Sets environment variables such as the port to run and Elixir production environment.

Sets environment variables such as the port to run and Elixir production environment. Line 16: Downloads release from S3 and extracts it.

Downloads release from S3 and extracts it. Line 25: Launches Elixir app in foreground.

We could test that our Docker build works with:

docker build --no-cache -t dockbit_phoenix_example:latest --build-arg RELEASE=1.0.0 .

Notice how we’re using Docker build-arg to pass the version of the release we want to run.

Setting up a CD pipeline

Sounds great, we’ve managed to build our Elixir app release, upload it to AWS S3 and build a Docker image. But hold on, do the architectures of the build and target machines match? Assuming you’ve been using your development machine and it isn’t Debian as defined in the Dockerfile, this Elixir application won’t be able to run. To fix this we’d need a similar build environment.

Luckily, Dockbit (our simple continuous delivery platform for the teams) has Elixir, AWS CLI and Docker integrations, necessary to set up such an automated pipeline. Let’s see how!

We’ve created a Dockbit pipeline of 3 stages, where we: