more info ⬇


SW engineering, engineering management and the business of software

subscribe for more
stuff like this:

2021 01 12

Deploying Elixir Phoenix webapps to the cloud with releases

I love many things about go. One of my favorite things is that cross-compilation is an absolute breeze. It takes just seconds and it’s especially easy with a tool like gox. After that, deployment is even easier. Just rsync or scp your executable binary and maybe a secret file or two. Boom💥 done. It’s essentially the easiest possible deployment story of any langauge or framework I know of.

So as a long time go developer (nearly a decade!) every other ecosystem pales in comparison.

Elixir is among them. Elixir itself is nearly a decade old, so there has been a lot of community work to make this better over time. As someone relatively new to this lovely language, I’m super grateful for all the hard work. Deploying basic Phoenix apps isn’t golang-easy, but’s it’s not painful.

This post details how I deploy a phoenix webapp, developed on a Mac to a single linux cloud instance.


First off, as you read about deploying elixir apps, you’ll see two primary release tools.

The first is an older tool that predates mix release. As I understand it, Distillery was essentially a community tool created to help with production deployments. mix release is realtively new, and is bundled with Elixir 1.9. Most of the new development and tooling is built around mix release. At least one long time Elixir dev I talked with seemed to think that mix release is the future.

I didn’t spend much time with Distillery, but mix release is a nice little tool. It essentially is able to package everything you need to run an Elixir app into a single directory. the layout looks like this:


The /my_app-1.2.3/bin/my_app is a simple shell script that you can use to start, stop, check version, launch in daemon mode, or even eval or call rpc expressions. /erts-x.y.z is the erlang VM. /lib is your app and all it’s dependencies. releases is a bunch of misc helper files, scripts, etc.

These release directories are essentially self-contained and can simply be scp’d or rsync’d to a remote cloud instance. rsync is preferred as most of it doesn’t change between releases. I typically see 100x or more speedups using rsync.

For this post, I added the following definitions to mix.exsproject definition:

def project do
    app: :my_app_,
    version: "0.3.2-a.2",


    default_release: :my_app,
    releases: [
      my_app: [
        include_executables_for: [:unix],
        include_erts: true,
        strip_beams: true,
        quiet: false,
        steps: [:assemble, :tar]

Read the Documentation!

Assuming you are deploying a webapp, you will definitely want to start with the Phoenix release documentation:

You are going to want to exit any files the above link tells you to edit. In particular, the stuff about prod.secret.exs and release.ex. The actuall steps are automated with scripts I’m detailing below.

For release.ex I had to add a line to make it work. Specifically, I needed to start :ssl in the load_app function:

defp load_app do

Additionally, the mix release hexdoc page has more details that help explain what is going on under the hood:

Definitely read the first, and I recommend reading both.


With Elixir, cross-compiling is theoretically possible, but it’s not tested and not supported in any way.

Update: I’ve since learned that cross-compilation may not be as dangerous as I first assumed. See here:

For the common case of building on a Mac or Windows machine and deploying to a linux cloud instance, you need a way to build on linux. Your basic strategies come down to:

For this post, I’ll walk thru the first.

For the second, just do a git checkout or rsync your project to the cloud instance and mix compile && mix release. You’ll have to make sure that elixir and any dependencies you need are installed on the cloud instance.

For the third, see the Phoenix documentation here.

With CI tools, reference your CI tool of choice to see if existing documentation exists or try to implement one of the above strategies via the CI tool.

Build in docker, spit out a tarball

First credit and thanks to Kai Wern Choong and their post about deploying Phoenix. It was super helpful to as I was learning about how to deploy with docker.

Here is the script I use to build a Phoenix app inside of a linux docker container, then spit out a tarball:


set -o nounset
set -o errexit

# build project in a dockerfile then output a tarball to sync with external server
# primarily using this post as a resource: 

export DATABASE_URL=`cat postgres.secret`
export SECRET_KEY_BASE=`mix phx.gen.secret`

docker build --build-arg DATABASE_URL --build-arg SECRET_KEY_BASE -t my_app_server .

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)"

id=$(docker create ${APP_NAME}_server)
docker cp $id:/app/${TAR_FILENAME} .
docker rm $id

mkdir -p _staging/${BASE_NAME}
mv ${TAR_FILENAME} _staging/${BASE_NAME}/.
cd _staging/${BASE_NAME}
tar -xf ${TAR_FILENAME}

It will simply create a directory called _release and put any release it builds in there.

The SECRET_KEY_BASE env variable is used for things like session tokens. In this script we regenerate everytime, but that will essentially log out all your users every release. SECRET_KEY_BASE should be persisted accross releases if you don’t want that behavior.

Here’s the Dockerfile I use:

Walking thru the Dockerfile we are essentially doing the following:


Next I have a simple script that deploys a version to an environment (dev|test|staging|prod). It’s not the most elegant script. But it’s my script and I love it:

This script does assume you have ssh keys or whatever setup.

Here’s what is going on in there:

Lastly, a fairly typical systemd service file (that is rsync’d by the script):


ExecStart=/home/USERNAME/my_app/bin/my_app start
ExecStop=/home/USERNAME/my_app/bin/my_app stop
ExecReload=/home/USERNAME/my_app/bin/my_app restart



I use caddy in front of the Phoenix app to get https working.

A v2 Caddyfile directive will look like this:

# reverse proxy {
    reverse_proxy localhost:4000
    encode zstd gzip

Caddy is the easiest way to get a Let’s Encrypt cert. It happens automatically. It’s basically a miracle compared to the hoops we used to jump thru before Let’s Encrypt was a thing.

All done

My time to automate deployment for my first Phoenix app was about a day and half.

The final steps would look like this:

A good chunk of it was just understanding what was going and debugging the Dockerfile (esp those ENV variables). My Docker-fu is a bit rusty these days.

Conceptually, nothing really hard is going on here. Using the above scripts and files as a template, that time to deploy should be much much shorter.

All the hard work was done by the Elixir community members who worked on the mix release functionality. Their work makes it so that I can deploy to a cloud instance without having to worry about what version of what package is installed on that cloud instance. I’m too spoiled to go back to the days where your release artifact has system dependencies. That way lies madness.

All in all, Elixir’s deployment story, like Elixir itself, has a bit of a learning curve, but ends up rather pleasant after you figure it out.

you should follow me on twitter at twitter/amattn and on at

you may also be interested in some of the greatest hits of

〜 The Sublime Developer Efficiency of Elixir, Phoenix and LiveView

〜 The Business Case For LiveView Is Strong Enough To Change How You Staff Your Dev Team

〜 Empathy as a Core Engineering Requirement

〜 Venture Capital Math 101

〜 What is Engineering Management?

the fine print:
〜 about 〜 archive 〜 twitter 〜 twitch 〜 consulting or speaking inquiries 〜
© matt nunogawa 2010 - 2021 / all rights reserved
back ⬆