Deployment, despite being an essential task, can be a confusing part of shipping an application. Depending on your stack, there could be a plethora of tools out there or… none at all. Unfortunately, Elixir falls into the latter bucket. Despite having a heart of gold, the language is still obscure, and that makes the process of deployment a tiny bit harder.
Addressing this problem may have been the reason for incorporating releases into version 1.9 of the language. Since the version bump, Elixir Releases have received the official blessing of the core language team. That means that deployment will finally be a piece of cake… right?
There’s a caveat. While releases are meant to be self-contained executables, they still call out to native system libraries to do things like open TCP sockets and write to files. That means that the native libraries referenced at compile time need to be exactly the same as the ones on your target machine. Unless you can guarantee that your workstation and cloud are exactly the same, releases can seem like only half the promise of a stress-free deployment.
Luckily, we can simulate the environment of our target machine with a Docker container. Let’s say we’re developing on a MacBook, but our target machine is running Ubuntu. What we’d need to do inside our container is:
The release compiled in the container should contain the proper native libraries to do all of the system-level stuff it needs to do.
Your Dockerfile could look something like this:
FROM elixir:1.9.4 # [1] | |
ARG env=dev # [2] | |
ENV LANG=en_US.UTF-8 \ # [3] | |
TERM=xterm \ | |
MIX_ENV=$env | |
WORKDIR /opt/build # [4] | |
ADD ./bin/build ./bin/build # [5] | |
CMD ["bin/build"] # [6] |
Let’s break this Dockerfile down step by step:
In order for our application to function properly, we’ll need to create directories, install dependencies, digest asset files (if we’re using Phoenix) — basically all of the manual tasks that we’d have to run through during a setup phase.
I found it easier to use a script to automate this. A minimal build could look like this:
#!/usr/bin/env bash | |
set -e | |
echo "Starting release process…" | |
cd /opt/build # [1] | |
echo "Creating release artifact directory…" | |
mkdir -p /opt/build/rel/artifacts # [2] | |
echo "Installing rebar and hex…" | |
mix local.rebar –force # [3] | |
mix local.hex –if-missing –force | |
echo "Fetching project deps…" | |
mix deps.get | |
echo "Cleaning and compiling…" | |
echo "If you are using Phoenix, here is where you would run mix phx.digest" | |
mix phx.digest | |
echo "Generating release…" | |
mix release # [4] | |
echo "Creating tarball…" | |
tar -zcf "/opt/build/rel/artifacts/PROJECT_NAME-PROJECT_VERSION.tar.gz" /opt/build/_build/prod # [5] | |
echo "Release generated at rel/artifacts/PROJECT_NAME-PROJECT_VERSION.tar.gz" | |
exit 0 |
Let’s dissect this file:
To bring this all together, we’ll need to build our container, and then run our build script inside it. But first, we need to make sure that our application actually starts the web server when it bootstraps.
If you’re not using Phoenix, feel free to skip down to where we create our container.
In order for the release executable to start your server, you have to add the following line to the config that corresponds to your mix environment:
# config/prod.secret.exs | |
config :my_project, MyProjectWeb.Endpoint, server: true |
At the time this post was written, this line was included by default in the prod.secret.exs file. If you can’t find it there, just add it yourself.
Remember those environment variables we referenced in our Dockerfile? Now is the time to pass them in. We’re going to use the --build-arg
argument to do this. Again, remember to chmod +x
this file:
#!/usr/bin/env bash | |
# This file is meant to be private! | |
# You should remove it from source control if | |
# you plan on using this repo. | |
docker build –build-arg env=prod \ | |
–build-arg env=prod \ | |
-t my-project:latest . |
One important note here: the tag you apply to your container will become important when we finally boot it up. Make sure you remember what it is.
Another note: it’s likely that this file will contain sensitive information (like the database url that Phoenix requires). I included this file here for the sake of completeness — if you choose to use this on a project of your own, I’d recommend hiding this file from version control.
Pro-tip: I aliased this script as mix pkg
in the mix.exs
file for convenience:
defp aliases do | |
[ | |
pkg: ["cmd ./local/build_container"] | |
] | |
end |
Run mix pkg
and make sure there are no errors. Once you’re done with that, we can run our build script inside our newly-built container.
Time to bring it around full-circle! We started this tutorial with a Dockerfile that referenced a directory. I mentioned that eventually we’d mount a shared volume between our host machine and the container on this directory. Well, now is that time:
#!/usr/bin/env bash | |
docker run -v $(pwd):/opt/build –rm -it my-project:latest /opt/build/bin/build | |
[1] [2] [3] |
Place the above command into a file and make it executable (I named it bin/generate_release). Now, let’s break it down:
(Note: I tried to alias this command as a mix task, but since Elixir requires terminal input to generate the release artifact, I wasn’t able to. But I could be wrong! If you can find or have found a way around this, please let me know!)
After running this command, you should see a .tar.gz
file inside your working directory. That is your release!
Feel free to run your app locally, or distribute it all over the Internet! All you need to run are two commands:
$ mix pkg
$ bin/generate_release
Thanks for reading. Check out the source for this article here, and feel free to contact me with any questions at prakash@carbonfive.com
We’re hiring! Looking for software engineers, product managers, and designers to join our teams in SF, LA, NYC, CHA.
Learn more and apply at www.carbonfive.com/careers