Deploying Phoenix for Elixir on AWS EC2

A guide to deploying an Elixir app using the Phoenix framework on AWS EC2 running Amazon Linux 2.

Sections

Background and Overview

Xkit is built on Phoenix, the popular framework for the Elixir language (which in turn runs on the Erlang VM).

When it came time to deploy our application, we were deciding between Gigalixir, Heroku, and AWS. Ultimately, although AWS would be a little bit more complicated to deploy, for some of the things we wanted to do (subdomain for each customer, custom domain support, etc) AWS was the better choice.

We also decided we wanted to deploy directly onto the EC2 instance rather than running the deployment in Docker. In our view, OTP handles a lot of the same jobs that Docker does, so adding Docker into the mix (no pun intended) is adding indirection, maintenance, and potentially performance bottlenecks.

For building the release, we settled on Elixir Releases. These seem to be the "way of the future" when it comes to deploying Elixir applications, and we wanted to futureproof our infrastructure setup.

However, what we assumed to be a pretty common scenario of deploying an Elixir Release to EC2 seemed to be largely undocumented. So we built some tools ourselves, and thought we'd share them.

Steps for Deployment

The following assumes that you have a Phoenix application on Elixir v1.9 or later that you're trying to deploy. It is based on the Phoenix guide to deployment so its recommended that you read that to get an understanding of the steps we're doing here.

Throughout this guide, we'll denote variables that you're intended to replace with double brackets, like `{{ replace_me }}`. (yes, this comes from our use of Jinja2 within Ansible!)

The Dockerfile

Elixir doesn't support cross-compilation, so to build an Elixir Release that targets Amazon Linux 2 for EC2, we either need to use another EC2 box to build our application, or use a Docker container.

We opted to use a Docker container so that we could keep our production box for use only in serving production traffic. We could have also spun up another EC2 instance to use just for building, but that felt like overkill.

The Dockerfile below targets Amazon Linux using the `amazonlinux:2` base image.

If you have any compile-time configuration in environment variables (e.g. in `config/prod.exs`), you'll need to add them as `ARG`s to the Dockerfile. (As an example, we use compile-time configuration to specify the HTTP port).

This Dockerfile is designed to live in the root directory of your application. If you decided to put it elsewhere (e.g. in a `deploy` directory, like we do), make sure you set the build context to the application root.

Building The Release

Now that we have our Dockerfile, we need to build and extract the Elixir Release.

First We need to grab our current OTP version:

We use this version to download Erlang, so we need both the major and minor version. See this StackOverflow answer for more details about why this is the best way to get both.

```shell
erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().' -noshell
```

and Elixir version:

```shell
elixir -e "IO.puts(System.version())"
```

Then we'll build the Docker image:

```shell
docker build -t {{ your_image_name }} \
               --build-arg OTP_VERSION={{ otp_version }} \
               --build-arg ELIXIR_VERSION={{ elixir_version }}
```

Don't forget, if you have other compile time environment variables, you'll need to pass them in here

With the Docker image built, we can create a temporary container and extract the release:

```shell
docker run --name {{ your_build_container }} {{ your_image_name }} sh
docker cp {{ your_build_container }}:/app/_build/prod/rel/{{ your_app_name }} ./temp
```

Now we've got an Elixir Release that targets Amazon Linux 2!

Move to EC2 and Start

All that's left is actually starting up the release.

First, on your EC2 box, create a location for the release to live.

```shell
mkdir -p ~/app
```

Copy the new release to EC2 (this is from your local machine):

```shell
scp ./temp/{{ your_app_name }} ec2-user@{{ your_ec2_host }}:~/app
```

And start up the new release:

```shell
~/app/bin/{{ your_app_name }} daemon_iex
```

You'll need to run the `daemon_iex` command with whatever environment variables you're using in `releases.exs`, like `DATABASE_URL`.

Wrap-up

With the steps above, you should be able to successfully deploy an Elixir Release for Phoenix to Amazon Linux on AWS EC2.

There are other steps to a successful deployment approach that we'll hopefully cover in future posts, such as:

  • Configuring EC2 and Phoenix to use alternate ports for HTTP/S to avoid running as root
  • Handling `RELEASE_COOKIE`
  • Automating the release (via Ansible)

Happy deploying!

Become an integrations expert.

Stay up to date on the latest articles about native integrations, new
Xkit features, and more by signing up for our mailing list.

Company-wide integrations for B2B SaaS with Xkit Groups

Xkit now supports groups to make it easy to build company-wide integrations for your customers.