This post describes the procedure to create lightweight Docker images, using multi-stage builds, to deploy Elixir applications packaged using Distillery.

It is assumed that you're familiar with Docker and Elixir.

Multi-stage builds

Since Docker version 17.05 you can have multi-stage builds. With such builds you can have a single Dockerfile contain multiple FROM instructions, separating multiple stages of a build, where artifacts from one stage can be used in the next and all resulting in a single image.

Example Use Case

You have a static site, like this blog, which is build using Hugo, and you want deploy it.

Your build dependencies are:

hugo

nodejs (because you also have some fancy JavaScript)

libsass (because you find vanilla css to be boring)

Your runtime dependencies are:

nginx (to serve the static html pages and assets)

Before multi-stage builds you'd either have a Dockerfile handling both building and serving the files, or 2 different ones commonly named Dockerfile.build and Dockerfile and run the build command twice like:

docker build -f Dockerfile.build .

and

docker build -f Dockerfile .

This process is now simplified with a Dockerfile like the following:

FROM node:latest as builder RUN apt-get -qq update RUN apt-get -qq install hugo libsass # Compile assets RUN npm run build WORKDIR /app # Generate static html pages RUN hugo FROM debian:jessie-slim RUN apt-get -qq update RUN apt-get -qq install nginx EXPOSE 80 # Notice this instruction # The generated files under public from the previous build step # are copied to the path which is served by nginx COPY --from = builder /app/public /var/www/html # Start nginx to serve files CMD [ "nginx" , "-g" , "daemon off" ]

To be able to use this new feature you have to make sure you have a version >= 17.05 installed:

A mininal Dockerfile which contains all the necessary steps to install Docker CE version 17.05 is the following:

FROM debian:jessie-slim MAINTAINER you@areawesome.com RUN apt-get -qq update RUN apt-get -qq install software-properties-common python-software-properties \ apt-transport-https ca-certificates curl gnupg2 # Add docker repository, for docker-ce edge (supports multi-stage builds) RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - RUN apt-key fingerprint 0EBFCD88 RUN add-apt-repository \ " deb [arch=amd64] https://download.docker.com/linux/debian \ $( lsb_release -cs ) \ edge " RUN apt-get -qq update # Install the minimum required version of docker for multi-stage builds RUN apt-get install -qq docker-ce = 17.05.0~ce-0~debian-jessie CMD [ "/bin/sh" ]

You may find the Dockerfile above handy, if you run a CI allowing you to supply your own Docker images, like GitLab CI.

Benefits

Packages required to build your release (eg. nodejs), increase the size of your image but aren't required during runtime.

You can install packages for debugging / tracing only to the final container.

The final container can be based off a slim image.

Dockerfiles are easier to maintain since it's clearer which package dependencies are required for the build phase and which are runtime ones.

Tutorial

For the example below, for the sake of simplicity, a Phoenix application without assets or database models is used. The application is named goo and you can generate it using the phoenix.new task below:

mix phoenix.new goo --no-brunch --no-ecto

About Distillery

Distillery is a deployment tool for Elixir applications, which reduces your Mix application to a single package, containing all dependencies and (optionally) the Erlang / Elixir runtime.

Configuring Distillery

You're suggested to take a moment to have a look at the distillery documentation about deploying phoenix applications.

Adding the dependency

The first thing we do, is to declare the distillery package dependency in mix.exs .

defmodule Goo.Mixfile do use Mix.Project def project do [ app : :goo , version : " 0.0.1 " , elixir : " ~> 1.2 " , elixirc_paths : elixirc_paths ( Mix . env ) , compilers : [ :phoenix , :gettext ] ++ Mix . compilers , build_embedded : Mix . env == :prod , start_permanent : Mix . env == :prod , deps : deps ( ) ] end def application do [ mod : { Goo , [ ] } , applications : [ :phoenix , :phoenix_pubsub , :phoenix_html , :gettext , :cowboy , :logger ] ] end defp elixirc_paths ( :test ) , do : [ " lib " , " web " , " test/support " ] defp elixirc_paths ( _ ) , do : [ " lib " , " web " ] defp deps do [ { :phoenix , " ~> 1.2.4 " } , { :phoenix_pubsub , " ~> 1.0 " } , { :phoenix_html , " ~> 2.6 " } , { :phoenix_live_reload , " ~> 1.0 " , only : :dev } , { :gettext , " ~> 0.11 " } , { :cowboy , " ~> 1.0 " } , # Distillery is added here { :distillery , " ~> 1.4.0 " } ] end end

Adding Distillery Config

You can generate the default distillery configuration using:

mix release.init

The generated rel/config.exs will be like:

Path . join ( [ " rel " , " plugins " , " *.exs " ] ) |> Path . wildcard ( ) |> Enum . map ( & Code . eval_file ( &1 ) ) use Mix.Releases.Config , # This sets the default release built by `mix release` default_release : :default , # This sets the default environment used by `mix release` default_environment : Mix . env ( ) # For a full list of config options for both releases # and environments, visit https://hexdocs.pm/distillery/configuration.html environment :dev do set dev_mode : true set include_erts : false set cookie : :" kool-thing " end environment :prod do set include_erts : true set include_src : false set cookie : :" song-for-karen " end release :goo do set version : current_version ( :goo ) set applications : [ :runtime_tools ] end

Dockerfile

FROM elixir:1.3.4-slim as builder RUN apt-get -qq update RUN apt-get -qq install git build-essential RUN mix local.hex --force && \ mix local.rebar --force && \ mix hex.info WORKDIR /app ENV MIX_ENV prod ADD . . RUN mix deps.get RUN mix release --env = $MIX_ENV FROM debian:jessie-slim ENV DEBIAN_FRONTEND noninteractive RUN apt-get -qq update RUN apt-get -qq install -y locales # Set LOCALE to UTF8 RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ locale-gen en_US.UTF-8 && \ dpkg-reconfigure locales && \ /usr/sbin/update-locale LANG = en_US.UTF-8 ENV LC_ALL en_US.UTF-8 RUN apt-get -qq install libssl1.0.0 libssl-dev WORKDIR /app COPY --from = builder /app/_build/prod/rel/goo . CMD [ "./bin/goo" , "foreground" ]

Build It

docker build -t goo:latest .

Run It

docker run -it goo:latest -name goo

Connect to the running node

docker exec -it goo /app/bin/goo remote_console

Why Debian Slim

You may have noticed that the base images for the final release image are based on Debian.

Debian is the distribution used for most official language base images:

Debian Slim applies some sane defaults to apt for Docker, like:

Keeping gzipped indexes

Not caching deb files under /var/cache/apt/archives Which means that you don't have to include the instruction below to reduce size:

RUN rm -rf /var/lib/apt/lists/* && apt-get clean

The final image for the above multi-stage Dockerfile is just 213MB, which is slim enough. If you're in desperate need to further reduce image sizes, you can use Alpine Linux, but then you're giving up the maturity and stability of:

glibc / GNU coreutils / Systemd

for..

musl / busybox / OpenRC

Further Steps

Adding distillery plugins to compile assets

Running migrations on deployment

Feel free to suggests edits and make comments on the reddit post.