Deploying Elixir (1 of 3): Building Releases With Mix
Tags: elixir, webdev, tutorial, aws
Published: 2020-06-18 17:58:45

---
title: Deploying Elixir (1 of 3): Building Releases With Mix
published: true
description: Build elixir releases locally with Docker & Mix. First in a series of 3 posts on deploying Elixir.
tags: elixir,webdev,tutorial,aws
---
This post is the first in a series of three on deploying elixir:

1. Building Releases with Docker & Mix
2. [Terraforming an AWS EC2 Instance](https://dev.to/jonlunsford/deploying-elixir-2-of-3-terraforming-an-aws-ec2-instance-1nh5)
3. [Deploying Releases with Ansible](https://dev.to/jonlunsford/deploying-elixir-3-of-3-provisioning-ec2-with-ansible-10o4)

---

Last time, we built a [small JSON endpoint with Plug, Cowboy and Poison](https://dev.to/jonlunsford/elixir-building-a-small-json-endpoint-with-plug-cowboy-and-poison-1826), let's retrofit that same app to build releases with Docker & Mix. The first step in deploying Elixir apps is to build them on the same target they will run. There are many ways to achieve this, we will walk through using Docker, locally, to build releases that can then be deployed.

If you did not follow along with that last post, you can grab the complete code here: [https://github.com/jonlunsford/webhook_processor](https://github.com/jonlunsford/webhook_processor)

---

#### Mix is:

>  A [build tool](https://hexdocs.pm/mix/Mix.html) that provides tasks for creating, compiling, and testing Elixir projects, managing its dependencies, and more.

Mix >= 1.9 introduced a task to manage releases, providing a built in alternative to tools like [Distillery](https://hexdocs.pm/distillery/home.html). We will be using this task to manage our releases: [`Mix.Tasks.Release`](https://hexdocs.pm/mix/Mix.Tasks.Release.html)

---

This is _how_ we will package our app for release. We will also write a [Mix task](https://hexdocs.pm/mix/Mix.Task.html) to:

1. Build a Docker _build_ image.
2. Build releases with Mix.

---

### Preparing our App for Release

Before we build our release with Docker, let's make a few changes to our app to get it ready for building releases. Update the `project` function in `mix.exs` for release:

```elixir
# ./mix.exs
defmodule WebhookProcessor.MixProject do
 ...
  def project do
    [
      app: :webhook_processor,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      releases: [ # add releases configuration
        prod: [ # we can name releases anything, this will be prod's config
          include_executables_for: [:unix], # we'll be deploying to Linux only
          steps: [:assemble, :tar] # have Mix automatically create a tarball after assembly
        ]
      ]
    ]
  end
 ...
end
```

Now we can begin building our release for our target environment.

---

### Add a /version Route

Let's make one more change to our endpoint so we can easily see which version of our app is ultimately running, this will come into play once we've actually deployed, add this to `./lib/webhook_processor/endpoint.ex`:

```elixir
defmodule WebhookProcessor.Endpoint do
  ...
  get "/version" do
    resp =
      case :application.get_key(:webhook_processor, :vsn) do
        {:ok, vsn} -> vsn
        _ -> "version not found :("
      end

    send_resp(conn, 200, resp)
  end
  ...
end
```

---

### Building the Docker Image

Given that Erlang, and thus Elixir, are compiled, we must target the environment our app will run in at _build time_. Fortunately, Docker makes this trivial. The general idea is this:

1. Create a Dockerfile that matches your target environment.
2. Build the release with Mix inside the Docker container.
3. Deploy the produced artifacts, the app tarball.

So let's do that, we will be _targeting_ a `Debian` environment. The minimal Dockerfile will look like this:

```dockerfile
# ./Dockerfile
# ENV matching production target host
# We'll be using a Debian linux EC2 instance to run this app
# See all official elixir docker images here: https://hub.docker.com/_/elixir
FROM elixir:1.10

# By default, if we're cutting a release it'll likely be prod
ARG ENV=prod

# We'll pass in ENV as a build arg to docker
ENV MIX_ENV=$ENV

# Our working directory within the container
WORKDIR /opt/build

# Add our release script to the container, named `release` and
# placed into the ./bin/ directory in our project root
ADD ./bin/release ./bin/release

# This is our entry point, make sure to run
# `chmod +x bin/release` to make this script executable
CMD ["bin/release", $ENV]
```

With this, we have what we need to build the Elixir image, next let's write our `release` bash script that will facilitate the Elixir build _inside_ our image. Be sure to make this file executable by running `chmod +x ./bin/release`:

```bash
#!/usr/bin/env bash

set -e

# The one arg that we can accept will be the ENV we are building
export MIX_ENV=$1

echo "Starting release process..."
cd /opt/build

echo "Installing rebar and hex..."
mix local.rebar --force
mix local.hex --if-missing --force

echo "Fetching project deps..."
mix deps.get --only prod

echo "Cleaning any leftover artifacts..."
mix do clean, compile --force

echo "Building $1 release..."
mix release $1 --overwrite

echo "Build completed!"
exit 0
```
---

### Building the Release

Given that building releases will be so common, let's write a Mix task to handle the redundancy. Our goal will be to have a task that reads like:

```bash
$ mix docker.build prod
```

It's common to create namespaces for mix tasks by defining them in their own file, here's how we get the `docker.build` task:

```elixir
# ./lib/mix/tasks/docker.build.ex
defmodule Mix.Tasks.Docker.Build do
  use Mix.Task

  @shortdoc "Docker utilities for building releases"
  def run(args) do
    Mix.Task.run("docker", args)
  end
end
```

The run function simply runs the `docker` task, passing along the args. The upper `Docker` module contains the build code:

```elixir
# ./lib/mix/tasks/docker.ex
defmodule Mix.Tasks.Docker do
  use Mix.Task
  use Mix.Tasks.Utils

  @shortdoc "Docker utilities for building releases"
  def run([env]) do
    # Build a fresh Elixir image, in case Dockerfile has changed
    build_image(env)

    # Get the current working directory
    {dir, _resp} = System.cmd("pwd", [])

    # Mount the working directory at /opt/build within the new elixir image
    # Execute the /opt/build/bin/release script
    docker(
      "run -v #{String.trim(dir)}:/opt/build --rm -i #{app_name()}:latest /opt/build/bin/release #{env}"
    )
  end

  defp build_image(env) do
    docker("build --build-arg ENV=#{env} -t #{app_name()}:latest .")
  end

  defp docker(cmd) do
    System.cmd("docker", String.split(cmd, " "), into: IO.stream(:stdio, :line))
  end
end

```

Here, we have a `run` function that matches the pattern `{env}`, you can now build your application by running: 

```bash
$ mix docker.build prod
```

After the script runs, you will have a tarball containing everything it needs to run on our `debian` target at:

```bash
_build/prod/prod-0.1.0.tar.gz
```

---

To recap, we've:

1. Created a Dockerfile to match our `Debian` target environment.
2. Created a build script that will run mix.
2. Created a mix task to facilitate everything.

With this approach you should now be able to build releases to match your target environment. As usual, the full code can be found on github: [https://github.com/jonlunsford/webhook_processor](https://github.com/jonlunsford/webhook_processor)


Up next, [Terraforming an EC2 instance](https://dev.to/jonlunsford/deploying-elixir-2-of-3-terraforming-an-aws-ec2-instance-1nh5) to deploy our app to.