Elixir meets Docker.io

In usual conditions it's preferable to deploy your Elixir / Erlang apps with exrm and a-la Capistrano derivatives like edeliver. There are cases when you either need to make it easily installable or handable to a client. In this rare cases packaging the app in the Docker image is worth the effort.

Here is how my Dockerfile looks:

# Dockerfile
FROM trenpixster/elixir:1.3.0

# Install AWS client (optional)
RUN apt-get update
RUN apt-get install -y awscli

RUN adduser --quiet deploy
WORKDIR /home/deploy

# Copy the app
COPY . app
RUN chown -R deploy:deploy app
WORKDIR app

USER deploy
ENV MIX_ENV prod

# This is to make sure it doesn't ask for Hex and Rebar
RUN mix local.hex --force
RUN mix local.rebar --force

# Build the release
RUN mkdir -p priv/static
RUN mix do deps.get, phoenix.digest
RUN touch config/prod.exs

# Prepare data directory
RUN mkdir -p /home/deploy/data/mnesia

EXPOSE 8080
EXPOSE 8443
VOLUME /home/deploy/data
ENTRYPOINT ["mix", "do", "compile,", "phoenix.server"]

Let's go section by section and I explain what's going on.

Base image. To create a runable Docker image I pick either official Elixir image (elixir) or the one by trenpixter (trenpixter/elixir). The last one tends to be very slightly ahead of the official.

# Dockerfile
FROM trenpixster/elixir:1.3.0

APT packages. If you need any system packages, you can install them as you usually would. Here I install AWS CLI package that I use to store backups to AWS S3.

# Install AWS client (optional)
RUN apt-get update
RUN apt-get install -y awscli

User setup. You obviously need a user that will run the app. Traditionally, I call it deploy, but it can be anything.

RUN adduser --quiet deploy
WORKDIR /home/deploy

Placing sources. Now the fun part begins. We need to get sources of the app into the box. I don't use any git repositories or anything for this, just copy sources from my working folder into the image.

# Copy the app
COPY . app
RUN chown -R deploy:deploy app
WORKDIR app

There are some files and directories that I don't want to be copied over and so I put them in a .dockerignore file that sits in the root of my local project tree:

# .dockerignore
.git
Dockerfile*
d-build
_build
deps
rel
Mnesia*
test

This one is typical, so you can grab it and add your own lines as necessary. Moving on.

Building the release. Now that the environment and sources are all ready we switch the user to the one that will be running the app and set the environment variable to production:

USER deploy
ENV MIX_ENV prod

This little hack is to make sure the building phase doesn't ask to install / update Hex and Rebar interactively. You certainly can't answer that.

# This is to make sure it doesn't ask for Hex and Rebar
RUN mix local.hex --force
RUN mix local.rebar --force

Finally, we create a directory for assets, fetch dependencies and build the app as part of the Phoenix static assets digesting.

# Build the release
RUN mkdir -p priv/static
RUN mix do deps.get, phoenix.digest
RUN touch config/prod.exs

The last line in the block above deserves some explanation. In this particular app I pass env variables to the app during the container launch. These vars customize behavior of the app and are part of the app configuration. If I don't touch the script, it won't be reconsidered during the app start and the app won't pick up new var values.

Launch configuration. In this app I use Mnesia database and as logic suggests, its data should be stored outside the box to survive the restarts and crashes. For this purpose I create a directory inside the user home /home/deploy/data that I then expose to the Docker environment as VOLUME. This volume will be mounted during the container start to a location in the host environment. Either default location Docker chooses for you, or your specific one.

Finally, I tell to expose two ports and specify the entry point that will be run when the container with this image launches -- mix do compile, phoenix.server:

# Prepare data directory
RUN mkdir -p /home/deploy/data/mnesia

EXPOSE 8080
EXPOSE 8443
VOLUME /home/deploy/data
ENTRYPOINT ["mix", "do", "compile,", "phoenix.server"]

Running the image

Here's how I run the image:

docker run --name myapp \
  -td \
  --restart=unless-stopped \
  -p 80:8080 \
  -p 443:8443 \
  -v /home/alg/volumes/myapp:/home/deploy/data \
  -e APP_VAR="1234" \
  image
  • I tell Docker to run this container with the name myapp.
  • It should be a detached TTY (we obviously want it to run in background).
  • It should be restarted upon crashes until explicitly stopped.
  • I bind ports 8080 and 8443 that this container provides to my host's 80 and 443, so that whatever app runs in it is exposed to the outside.
  • The volume /home/deploy/data that was defined in the Docker file is bound to the host's directory /home/alg/volumns/myapp.
  • I pass an environment var APP_VAR to the container. This var is used in the app.
  • And finally, here's the name of the image we built with that Dockerfile above.

Have fun!

comments powered by Disqus