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 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.
# 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
- 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/datathat was defined in the Docker file is bound to the host’s directory
- I pass an environment var
APP_VARto the container. This var is used in the app.
- And finally, here’s the name of the image we built with that