Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thbar/6763e821d9c9a81d78830d9a79b1685b to your computer and use it in GitHub Desktop.
Save thbar/6763e821d9c9a81d78830d9a79b1685b to your computer and use it in GitHub Desktop.

Using Elixir releases and multi-stage Docker files to simplify Phoenix deployment

This repo is my experiment in deploying a basic Phoenix app using the release feature from elixir 1.9 (https://elixir-lang.org/blog/2019/06/24/elixir-v1-9-0-released/) and docker, via a multi-stage Dockerfile (https://docs.docker.com/develop/develop-images/multistage-build/) leveraging bitwalker's docker images for Elixir and Phoenix.

Step 1: Install Elixir 1.9.1 (and Erlang)

The simplest way to manage Elixir versions is to use asdf. Aside: you can use it to manage the versions of many languages so you could use it as a replacement for rvm/rbenv, pyenv etc. Since discovering it, I use it to maintain most of my language installs

A simple brew install asdf does the trick. If you don't use brew, use git:

$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.7.4

Now we need to install the plugin for elixir:

$ asdf plugin-add elixir

If you're curious to see all the versions available, do:

$ asdf list-all elixir

Sweet, we've confirmed 1.9.1 is in the list so...:

$ asdf install elixir 1.9.1

Elixir relies on Erlang so we need to install that too:

$ asdf plugin-add erlang
$ asdf install erlang 22.1

For erlang and elixir to show up on your path, you need to install the asdf helper in your profile. Add this toward the end of your .bash_profile (or equivalent for your trendy shell):

$HOME/.asdf/asdf.sh

Fire up a new terminal and do a simple sanity check:

$ elixir -v

and you should see something like:

Erlang/OTP 22 [erts-10.4.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.9.1 (compiled with Erlang/OTP 20)

Step 2 Install Phoenix

The precursor to install Phoenix is to install the Elixir package manager, Hex. Roughly speaking, mix is the Elixir equivalent of rake so to install hex via mix, we do:

$ mix local.hex

With hex, we can now install Phoenix:

$ mix archive.install hex phx_new 1.4.10

Step 3 Creating a bare bones Phoenix app

To create a simple bare bones Phoenix app called hello:

$ mix phx.new hello --no-ecto --no-webpack

Answer Y to install all the necessary dependencies. (The omission of webpack saves 200M. O_o)

To kick the tires, do:

$ cd hello
$ mix phx.server

Now surf to localhost:4000 and you should see the Phoenix welcome page. Great success!

Step 4: Getting started with Elixir releases

The reason we opted for the newest version of Elixir is we want to leverage a new feature called releases (https://elixir-lang.org/blog/2019/06/24/elixir-v1-9-0-released/Releases) which simplify deployment by generating a minimal self-contained payload ideal for creating a lightweight docker container.

Our first step is to transition to away from Mix.Config to Elixir's built-in Config package. Visit all the .exs file in the config directory and replace use Mix.Config at the top of the file with require Config. Part of the transition to Release was to bring the Mix.Config functionality inside Elixir hence the removal of the Mix. suffix.

Now let's try to build our first production release:

$ mix phx.digest # Digests and compresses static files
$ MIX_ENV=prod mix release
** (RuntimeError) environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
...

Oops! Well the error is self-explanatory, at least. The easy fix is:

$ export SECRET_KEY_BASE=`mix phx.gen.secret`

Now it builds successfully. Sweet. And there's a nice explainer printed about the various ways of using your shiny new release. Let's kick the tires with:

_build/prod/rel/hello/bin/hello start

If we hit our usual endpoint, localhost:4000, we get nada. Turns out, that by default, Phoenix doesn't kick off the server in the production environment. So edit, config/prod.exs and add the following line to the end:

config :hello, HelloWeb.Endpoint, server: true

Rebuild the release and try again. This time, you should see the Cowboy webserver start message:

14:59:59.989 [info] Running HelloWeb.Endpoint with cowboy 2.6.3 at :::4000 (http)
14:59:59.990 [info] Access HelloWeb.Endpoint at http://example.com

What's cool, is that you can now connect to it remotely in another terminal with:

$ _build/prod/rel/hello/bin/hello remote

Neato!

Step 5: Multi stage docker file and Elixir 1.9

So now that we have our minimal self-contained (no need for a separate Erlang or Elixir installation!) release, we are in good shape to deploy it using a Docker container. The good news is that someone, bitwalker (aka Paul Schoenfelder), has already produced a minimal container for Phoenix Elixir. So let's look at a Dockerfile to build our release

FROM bitwalker/alpine-elixir-phoenix:1.9.1 as builder

ARG secret

ENV MIX_ENV=prod SECRET_KEY_BASE=$secret

WORKDIR /opt/app

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

RUN mix do deps.get --only prod, deps.compile, phx.digest, release

We leverage @bitwalker's Docker container for Elixir and Phoenix that's based off the minimal Alpine Linux. We copy across the relevant parts of our Phoenix project, i.e. config, lib and priv and our dependencies (mix.exs). Then we build our project using mix:

  • deps.get --only prod, gets only the dependencies needed for prod.
  • deps.compile, compiles the dependencies.
  • phx.digest, digests and compresses static files.
  • release, builds our release. Our release should be complete and located in /opt/app/_build.

The next step is to kick off a new Docker container build and copy over only the bits necessary to run our project. The only trickiness here is that we must make sure that we use the same Alpine version as @bitwalker used to create his elixir/phoenix container, i.e. 3.10.2, and install bash and openssl. Then we copy over the _build directory and run it:

FROM alpine:3.10.2 as runner

RUN apk update && apk add --no-cache bash openssl

WORKDIR /opt/app

COPY --from=builder /opt/app/_build .

CMD trap 'exit' INT; ./prod/rel/hello/bin/hello start

Now we can fire up our app with:

$ docker build -t runner .

And run it with:

$ docker run --publish 5555:4000 --env SECRET_KEY_BASE=$(mix phx.gen.secret) runner:latest
22:31:51.156 [info] Running HelloWeb.Endpoint with cowboy 2.6.3 at :::4000 (http)
22:31:51.156 [info] Access HelloWeb.Endpoint at http://example.com
22:32:13.567 request_id=Fc3yu9Tt-EFBNAoAAAAC [info] GET /
22:32:13.567 request_id=Fc3yu9Tt-EFBNAoAAAAC [info] Sent 200 in 231µs

You can see it running:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
747daafcd12a        runner:latest       "/bin/sh -c 'trap 'e…"   4 minutes ago       Up 4 minutes        0.0.0.0:5555->4000/tcp   hello_deployed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment