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.
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)
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
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!
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!
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