My earlier attempts at setting up reliable and repeatable Elixir application deployments worked, but I didn’t feel completely safe. I really tried to setup the deployment of my Phoenix app “correctly”, sticking closely to “the standard” - Erlang/OTP, hot code reloading, upgrade releases and automatic versions. About 1/4 of the time upgrades simply didn’t work. The process being so reliable failed like a professional soldier: no complaining, no screaming and no emotions. edeliver would tell me that everything worked well, but the new code “wasn’t taking”: my changes were not visible on the website until I manually restarted the app on the server. BTW, this is why (as of this writing) this website displays app version in the footer of each page. After hours of troubleshooting I couldn’t figure out a pattern for these seemingly random failures.

Check out my last 2 blog posts for full distillery + edeliver deployment configuration walkthrough:

Shortly after I finished my last post about Elixir upgrade releases, I came across this wonderfully useful article by Tymon Tobolski about a tool he created - mix_docker.

This post is the complete walkthrough of the Phoenix app deployment using mix_docker. Some of the material here reiterates Tymon’s post while adding much more detail specific to packaging a Phoenix app in a Docker image.

Docker logo

We are going to:

  1. Create brand new Phoenix app.
  2. Add mix_docker.
  3. Customize docker images.
  4. Configure your app with ENV variables.
  5. Run your app.
  6. Draw conclusions. :)

This guide assumes you already have Docker, Elixir and Phoenix Framework installed on your machine.

mix_docker is a hex package that drastically simplifies the packaging of Elixir releases into a minimal Docker container. The key trick here is to split the construction of your production image into 2 steps:

  1. Use a “build image” to compile everything (Elixir code + assets) and build an Erlang release.
  2. Create a “release image” and put Erlang release in it.

Quick refresher from distillery documentation on what Erlang release is:

A release is a package of your application’s .beam files, including it’s dependencies .beam files, a sys.config, a vm.args, a boot script, and various utilities and metadata files for managing the release once it is installed. A release may also contain a copy of ERTS (the Erlang Runtime System).

distillery is the most popular hex package that mix_docker uses (depends on) to build Erlang releases.

Build image must have a lot of software installed on it in order to build the release: Erlang, Elixir, NodeJs, etc., hence the image size = large. Release image only needs a matching version of Erlang installed. This is how a release image can be very small. In fact, a release image doesn’t even have to have Erlang installed if you choose to include the Erlang runtime in your application’s release (default distillery setting for production environment).

1. Create brand new Phoenix app

mix phoenix.new hi_docker

It is ok to use an existing app too.

2. Add mix_docker

def deps do
  [{:mix_docker, "~> 0.3.0"}]
end

Set the name for the Docker image in config/config.exs:

# config/config.exs
config :mix_docker, image: "hi_docker"

Run mix deps.get to install the added hex package.

Run mix docker.init to create default distillery release configuration in rel/config.exs.

Edit rel/config.exs:

# Don't bundle Erlang runtime,
# because it would already be installed in the release image
environment :prod do
  set include_erts: false # set to false.
  # ...

Edit .dockerignore, add the following lines:

node_modules
priv/static
hi_docker.tar.gz

Add hi_docker.tar.gz to your .gitignore as well.

We’re going to compile and digest static assets inside of our build image, that way both build image and release image could be built on CI server - as recommended by Tymon himself.

3. Customize docker images

Run mix docker.customize. This will copy the default Dockerfile.build and Dockerfile.release into your app’s root directory.

Add the following packages to Dockerfile.build using standard Dockerfile commands:

  • nodejs
  • python

Install nodejs dependencies and cache them by adding the following lines before the COPY command:

# Cache node deps
COPY package.json ./
RUN npm install

Build and digest static assets by adding the following lines after the COPY command:

RUN ./node_modules/brunch/bin/brunch b -p && \
    mix phoenix.digest

Test the Dockerfile.build:

mix docker.build

Complete listing of Dockerfile.build:

FROM bitwalker/alpine-erlang:6.1

ENV HOME=/opt/app/ TERM=xterm

# Install Elixir and basic build dependencies
RUN \
    echo "@edge http://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
    apk update && \
    apk --no-cache --update add \
      git make g++ nodejs python \
      elixir@edge && \
    rm -rf /var/cache/apk/*

# Install Hex+Rebar
RUN mix local.hex --force && \
    mix local.rebar --force

WORKDIR /opt/app

ENV MIX_ENV=prod

# Cache elixir deps
COPY mix.exs mix.lock ./
RUN mix do deps.get, deps.compile

# Cache node deps
COPY package.json ./
RUN npm install

COPY . .

RUN ./node_modules/brunch/bin/brunch b -p && \
    mix phoenix.digest

RUN mix release --env=prod --verbose

4. Configure your app with ENV variables

The best way to provide runtime configuration is via environment variables. Remember The Twelve-Factor App? These principles still apply here.

Remove config/prod.secret.exs file and remove a reference to it from config/prod.exs. Configure your app’s secrets directly in config/prod.exs using environment variables:

# config/prod.exs
#
# Configure your app's endpoint.
config :hi_docker, HiDocker.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [host: "${HOST}", port: {:system, "PORT"}],
  secret_key_base: "${SECRET_KEY_BASE}",
  cache_static_manifest: "priv/static/manifest.json",
  server: true,
  root: ".",
  version: Mix.Project.config[:version]

# Configure your database
config :hi_docker, HiDocker.Repo,
  adapter: Ecto.Adapters.Postgres,
  hostname: "${DB_HOST}",
  database: "${DB_NAME}",
  username: "${DB_USER}",
  password: "${DB_PASSWORD}",
  pool_size: 20

5. Run your app

No changes are needed for the default Dockerfile.release - it works as is!

Build production image:

  1. mix docker.build
  2. mix docker.release

Run your production image!

docker run -it --rm -p 8080:8080 -e PORT=8080 -e HOST=<domain-name> -e DB_HOST=<postgresql-domain> -e DB_NAME=hi_docker -e DB_USER=<postgresql-user> -e DB_PASSWORD=<postgresql-password> -e SECRET_KEY_BASE=<top-secret> hi_docker:release foreground

The above command starts a Docker container using your release image - hi_docker:release. It simply runs the Erlang release with your app in the “foreground” mode.

Breakdown of the switches:

  • -it a combination of 2 docker run switches: “-i” and “-t” to run your container in the “interactive” mode with TTY allocated, so that you could stop your container by pressing Ctrl + C.
  • --rm tells docker to delete the container automatically after it is stopped. By default docker does not delete stopped containers, you could either delete them manually (docker rm CID) or use --rm to prevent “container pollution”.
  • -p 8080:8080 maps port 8080 on your machine to port 8080 inside of the docker container.
  • -e PORT=8080 sets environment variable PORT to 8080 inside of the container.
  • -e HOST=<domain-name> sets the ENV variable to be used by your app to generate URLs, this is your website’s domain name!
  • -e DB_HOST and related DB_ variables - no explanation needed.
  • -e SECRET_KEY_BASE=<top-secret> another ENV variable used by Phoenix to verify integrity of cookies.
  • hi_docker:release name of your release image and a tag.
  • foreground the argument for your app’s release executable, to tell Erlang to run your app in “foreground” mode: it logs everything into STDOUT and lets you stop the app by pressing Ctrl + C.

Postgresql settings

I assume you’re running your production release image on you personal computer (for now) and Postgresql database server runs locally. By default Posgresql only allows connections from localhost. Your app is running inside of a container where the localhost means a different thing: you need to configure your Postgresql to allow remote connections. If you’re running the macOS and Postgresql is installed via Homebrew, then your postgres config is likely located in /usr/local/var/postgres: both pg_hba.conf and postgresql.conf.

Ready for production?

Head over to Tymon’s post on setting up Elixir Cluster Using Docker and Rancher.

6. Conclusions

As of this writing, this website is not running on Docker - it is still deployed using “classic” Erlang upgrade releases. My plan is to migrate to Docker, which would hopefully let me switch the focus from deployment to development :)

I feel a lot better about deploying my Phoenix apps with Docker: it is reliable and repeatable, exactly what deployment must be. I can build my release image on my personal computer and on a CI server, as part of CI build. I can run my release image anywhere Docker runs.

There ARE legitimate uses for hot code upgrades. Barry Jones from Codeship pointed out in his blog post that upgrade releases are “a little bit more complex” and distillery “goes out of its way to make this easy… but that still doesn’t mean you should always use it.”

How do you deploy your Phoenix apps?