Elixir, Erlang, Docker, Ansible

Deploying with Distillery and Docker

At SmartLogic we use Distillery to generate Erlang releases for our Elixir applications. Through this post I'll explain how we generate the releases that get shipped to production.

We use Docker to ensure that the final Erlang release is always the same across production instances. This approach also allows us to avoid generating the release on production, which means our production instances stay slimmer by not having Erlang, Elixir, and Node installed natively.

Release Script

To start with, we have a release.sh script that builds a Docker image and then copies the resulting release out of the image into a local tmp folder.

Below is the entire release script.

#!/bin/bash
set -e

working_dir=$(pwd)

if [[ $working_dir == *"/deploy" ]]; then
  cd ..
fi

mkdir -p deploy/tmp/

docker build -f Dockerfile.releaser -t app:releaser .

DOCKER_UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
RELEASE_FILE_PATH=/app/_build/prod/rel/app/releases/0.1.0/app.tar.gz

docker run --name app_releaser_${DOCKER_UUID} app:releaser /bin/true
docker cp app_releaser_${DOCKER_UUID}:${RELEASE_FILE_PATH} deploy/tmp/
docker rm app_releaser_${DOCKER_UUID}

This file is typically run by Ansible, which we run from the deploy folder of our applications, so the script runs a quick check to confirm that the current working directory is the deploy folder and then changing folders to be in the root of the project. This is important because Docker needs to have the root folder as the context for building.

Docker can only copy out of instances of images called containers, so we create a new container by running /bin/true with a unique name. Each time the script runs we generate a universally unique identifier (UUID) for the new container in order to avoid collisions and ensure that we always know which release we're pointing at.

The Dockerfile

Let’s walk through the dockerfile in its stages, first the back end and then the front.

FROM elixir:1.8 as builder

RUN mix local.rebar --force && \
    mix local.hex --force

WORKDIR /app
ENV MIX_ENV=prod
COPY mix.* /app/
RUN mix deps.get --only prod

RUN mix deps.compile

To start with for the back end we add and install the base Elixir dependencies; hex, rebar, and the libraries required. If the dependencies don’t change in mix.exs or mix.lock, then these steps are skipped in future builds.

FROM node:11.2 as frontend

WORKDIR /app
COPY assets/package.json assets/yarn.lock /app/
COPY --from=builder /app/deps/phoenix /deps/phoenix
COPY --from=builder /app/deps/phoenix_html /deps/phoenix_html

RUN npm install -g yarn && yarn install

COPY assets /app
RUN npm run deploy

Next we have the front end, this step follows the same pattern as the Elixir dependencies but for node. We copy the package.json and yarn.lock file over first, so that if the dependencies don’t change, we can skip these steps in future builds.

Make sure if you're using Yarn as your dependency manager that you include the yarn.lock and not the package-lock.json file. This is very important, otherwise your production assets will have unlocked versions and may upgrade themselves unexpectedly.

FROM builder as releaser
COPY --from=frontend /priv/static /app/priv/static
COPY . /app/
RUN mix phx.digest
RUN mix release --env=prod

Finally we have the step that pulls the front end and back end together into the final release. All of the files are copied into the image and then mix release is run. This final step generates the tarball that we copy out from the release script.

Conclusion

Generating an Erlang release via Docker has been very stable for me, all of my side projects use this in addition to our new client work here at SmartLogic. This way we can generate a single release and push it to as many production servers as we want. Production servers with this flow do not require any dev tools to be installed.

For a real example that I use to deploy Grapevine, you can find the release.sh script and the Dockerfile.releaser on GitHub.

For a walkthrough of how we configure Distillery, including an archive of the live stream demo, check out our post on Configuring Distillery.

Photo by frank mckenna on Unsplash