Distillery releases with Docker multi-stage builds
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:
- Download Docker CE Edge (Mac)
- Read how to install Docker CE on a Linux Server
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..
Further Steps
- Adding distillery plugins to compile assets
- Running migrations on deployment
Feel free to suggests edits and make comments on the reddit post.