A complete guide to deploy an Elixir Phoenix application to Kubernetes - Part 1 - Prepare for deployment

18.01.20194 Min Read — In DevOps

In this first article, we will share about Docker experience when dockerize an Elixir/Phoenix application and using Distillery 2 to build a release.

Prerequisites

Please check out the application BlogWorld that we will go in this series includes

  • Phoenix 1.4
  • PostgreSQL
  • Redis

Install Distillery 2

  • Add distillery hex package to your mix.exs file
{:distillery, "~> 2.0"}
  • Run mix deps.get to install the dependency. And then run mix release.init to init Distillery configuration
mix release.init

Dockerize application

The Distillery creator actually wrote a post about Deploying with Docker - it's pretty good, or you guys can see our Dockerfile:

FROM elixir:alpine AS builder

# The name of your application/release (required)
ARG APP_NAME

# The version of the application we are building (required)
ARG APP_VSN

# Set this to true if this release is not a Phoenix app
ARG SKIP_PHOENIX

# If you are using an umbrella project, you can change this
# argument to the directory the Phoenix app is in so that the assets
# can be built
ARG PHOENIX_SUBDIR=.

ENV SKIP_PHOENIX=${SKIP_PHOENIX} \
  APP_NAME=${APP_NAME} \
  APP_VSN=${APP_VSN} \
  REPLACE_OS_VARS=true \
  TERM=xterm \
  MIX_ENV=prod

# By convention, /opt is typically used for applications
WORKDIR /opt/app

# This step installs all the build tools we'll need
RUN apk update \
  && apk --no-cache --update add nodejs nodejs-npm \
  && mix local.rebar --force \
  && mix local.hex --force

# This copies our app source code into the build container
COPY . .

RUN mix do deps.get, deps.compile, compile

# This step builds assets for the Phoenix app (if there is one)
# If you aren't building a Phoenix app, pass `--build-arg SKIP_PHOENIX=true`
# This is mostly here for demonstration purposes
RUN if [ ! "$SKIP_PHOENIX" = "true" ]; then \
  cd ${PHOENIX_SUBDIR}/assets && \
  npm install && \
  npm run deploy && \
  cd .. && \
  mix phx.digest; \
  fi

RUN \
  mkdir -p /opt/built && \
  mix release --env=prod --verbose && \
  cp _build/${MIX_ENV}/rel/${APP_NAME}/releases/${APP_VSN}/${APP_NAME}.tar.gz /opt/built && \
  cd /opt/built && \
  tar -xzf ${APP_NAME}.tar.gz && \
  rm ${APP_NAME}.tar.gz

# From this line onwards, we're in a new image, which will be the image used in production
FROM alpine:latest

# The name of your application/release (required)
ARG APP_NAME

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

ENV PORT=4000 \
  MIX_ENV=prod \
  REPLACE_OS_VARS=true \
  APP_NAME=${APP_NAME}

WORKDIR /opt/app

EXPOSE ${PORT}

COPY --from=builder /opt/built .

CMD ["/opt/app/bin/${APP_NAME}", "foreground"]

This Dockerfile must follow two best practices to help us reduce container image size, avoid additional overhead are wasted space and great hiding place for security vulnerabilities and bugs.

  • Using Small Base Images: It's probably easiest way to reduce your container size. For example: Take a look the Elixir image from DockerHub:

    • Elixir:1.7 -> ~466 MB
    • Elixir:1.7-alpine -> ~52 MB

    alpine image reduces our base image size by 9 times.

    Almost programming languages supports alpine images.

  • Using Builder Pattern: Elixir is a compiled language, the source code first is turned into compiled code beforehand. The compilation step often requires tools that are not needed to actually run the code, this mean you can actually remove these tools. To do this, you can use the builer pattern.

    The code is built in the first container which is elixir-alpine image, and then the compiled code is packaged in the final container which is raw alpine image without all the Elixir compilers and tools required to make the Elixir compiled code.

In the end, our container image only 59.3 MB. Amazing!!!

Scripting building process

From the Dockerfile, there are some ARGs that we need to pass when running docker build. Makefile will help us run commands easier

APP_NAME ?= `grep 'app:' mix.exs | sed -e 's/\[//g' -e 's/ //g' -e 's/app://' -e 's/[:,]//g'`
APP_VSN ?= `grep 'version:' mix.exs | cut -d '"' -f2`
BUILD ?= `git rev-parse --short HEAD`

help:
		@echo "$(APP_NAME):$(APP_VSN)-$(BUILD)"
		@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

build: ## Build the Docker image
		docker build --build-arg APP_NAME=$(APP_NAME) \
				--build-arg APP_VSN=$(APP_VSN) \
				--build-arg SKIP_PHOENIX=false \
				-t $(APP_NAME):$(APP_VSN)-$(BUILD) \
				-t $(APP_NAME):latest .

Code checkpoint

You guys can check out the code of this part at here or tag part-1.