Deploy an Elixir Phoenix Application

Getting an application running on Fly is essentially working out how to package it as a deployable image and attach it to a database. Once packaged it can be deployed to the Fly infrastructure to run on the global application platform.

In this guide we'll learn how to deploy an Elixir Phoenix application on Fly and connect it to a PostgreSQL database.

This guide is for apps generated on Phoenix 1.6.3 or later, where deployment is streamlined significantly. If you're on an earlier version and can't upgrade, check out our previous deployment guide.

We'll be using the standard web application generated by the Elixir Phoenix Framework.

Preparation

If you don't already have Elixir and Phoenix installed, get them set up before starting this guide. When you install Elixir, the Mix build tool ships with it.

You can install the Phoenix project generator phx.new from a Hex package as follows:

mix archive.install hex phx_new

Generate the App and Deploy With Postgres

If you just want to see how Fly.io deployment works, this is the section to focus on. Here we'll bootstrap the app and deploy it with a Postgres database.

First, install flyctl, your Fly app command center, and sign up to Fly if you haven't already.

Now let's generate a shiny, new Phoenix app:

mix phx.new hello_elixir

The output ends with instructions for configuring the database and starting the app manually. The Fly.io launcher is going to take care of this, so we can ignore them.

When we run fly launch from the newly-created project directory, the launcher will:

  • Ask you to select a deployment region
  • Set secrets required by Phoenix (SECRET_KEY_BASE, for example)
  • Run the Phoenix deployment setup task
  • Optionally setup a Postgres instance in your selected region
  • Deploy the application in your selected region

cd hello_elixir
fly launch
Creating app in /Users/me/hello_elixir
Scanning source code
Detected a Phoenix app
? App Name (leave blank to use an auto-generated name): hello_elixir
? Select organization: flyio (flyio)
? Select region: mad (Madrid, Spain)
Created app hello_elixir in organization soupedup
Set secrets on hello_elixir: SECRET_KEY_BASE
Installing dependencies
Running Docker release generator
Wrote config file fly.toml
? Would you like to setup a Postgres database now? Yes
Postgres cluster hello_elixir-db created
  Username:    postgres
  Password:    <password>
  Hostname:    hello_elixir-db.internal
  Proxy Port:  5432
  PG Port: 5433
Save your credentials in a secure place, you will not be able to see them again!

Monitoring Deployment

1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 2 total, 2 passing] --> v0 deployed successfully

Connect to postgres Any app within the flyio organization can connect to postgres using the above credentials and the hostname "hello_elixir-db.internal." For example: postgres://postgres:password@hello_elixir-db.internal:5432

See the postgres docs for more information on next steps, managing postgres, connecting from outside fly: https://fly.io/docs/reference/postgres/ Postgres cluster hello_elixir-db is now attached to hello_elixir

Would you like to deploy now? Yes Deploying hello_elixir

==> Validating app configuration --> Validating app configuration done Services TCP 80/443 ⇢ 8080 Remote builder fly-builder-little-glitter-8329 ready ...

...

That's it! Run fly open to see your deployed app in action.

Try a few other commands:

  • fly logs - Tail your application logs
  • fly status - App deployment details
  • fly status -a hello_elixir-db - Database deployment details
  • fly deploy - Deploy the application after making changes

Storing Secrets on Fly.io

You may also have some secrets you'd like set on your app.

Use fly secrets to configure those.

fly secrets set MY_SECRET_KEY=my_secret_value

Deploying Again

When you want to deploy changes to your application, use fly deploy.

fly deploy

Note: On Apple Silicon (M1) computers, docker runs cross platform builds using qemu which might not always work. If you get a segmentation fault error like the following:

 => [build  7/17] RUN mix deps.get --only
 => => # qemu: uncaught target signal 11 (Segmentation fault) - core dumped

You can use fly's remote builder by adding the --remote-only flag:

fly deploy --remote-only

You can always check on the status of a deploy

fly status

Check your app logs

fly logs

If everything looks good, open your app on Fly.io!

fly open

Important IPv6 Settings

The flyctl command attempts to modify your project's Dockerfile and append the following lines:

# Appended by flyctl
ENV ECTO_IPV6 true
ENV ERL_AFLAGS "-proto_dist inet6_tcp"

If you customized your Dockerfile or launched without the Dockerfile, this setting may not have been set for you. These values are important and enable your Elixir app to work smoothly in Fly's private IPv6 network.

Check for this If you encounter network related errors like this:

Could not contact remote node my-app@fdaa:0:31d4:a5b:9d36:7c1e:f284:2, reason: :nodedown. Aborting...

IEx Shell Into Your Running App

Elixir supports getting a IEx shell into a running production node. How cool is that?

To do this, we will login with SSH to our application VM. There are a couple one-time setup tasks for using SSH. Follow the instructions.

fly ssh establish
fly ssh issue --agent

With SSH configured, let's open a console.

fly ssh console
Connecting to hello_elixir.internal... complete
/ #

If all went smoothly, you have a shell to the machine!

Now we just need to launch our remote IEx shell. The command is named for your application. For this example it is:

app/bin/hello_elixir remote
Erlang/OTP 23 [erts-11.2.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]

Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(hello_elixir@fdaa:0:1da8:a7b:ac4:b204:7e29:2)1>

You have a live IEx shell into your application!

(Tip: One of several ways to exit the IEx shell is to hit Ctrl+C, Ctrl+C; to log out of the VM console, use Ctrl+D or exit.)

With another quick update we can prepare our application for clustering by naming our nodes differently. That's next!

Naming Your Elixir Node

In your Elixir application, run this command:

mix release.init

Then edit the generated rel/env.sh.eex file and add the following lines:

ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=$FLY_APP_NAME@$ip

This names our Elixir node using the Fly application name and the internal IPv6 address. Make sure to deploy after making this change!

fly deploy

Nice! Our application is ready for clustering!

Clustering Your Application

Elixir and the BEAM have the incredible ability to be clustered together and processes can pass messages seamlessly to each other between nodes. Fly makes clustering easy! This extra (and totally optional) portion of the guide walks you through clustering your Elixir application.

There are 2 parts to getting clustering quickly setup on Fly.

  • Installing and using libcluster
  • Scaling our application to multiple VMs

Adding libcluster

The widely adopted library libcluster helps here.

Libcluster supports multiple strategies for finding and connecting with other nodes. The strategy we'll use is DNSPoll which was added in version 3.2.2 of libcluster, so make sure you're using that version or newer.

After installing libcluster, add it to the application like this:

defmodule HelloElixir.Application do
  use Application

  def start(_type, _args) do
    topologies = Application.get_env(:libcluster, :topologies) || []

    children = [
      # ...
      # setup for clustering
      {Cluster.Supervisor, [topologies, [name: HelloElixir.ClusterSupervisor]]}
    ]

    # ...
  end

  # ...
end

Our next step is to add the topologies configuration to the file config/runtime.exs.

  app_name =
    System.get_env("FLY_APP_NAME") ||
      raise "FLY_APP_NAME not available"

  config :libcluster,
    debug: true,
    topologies: [
      fly6pn: [
        strategy: Cluster.Strategy.DNSPoll,
        config: [
          polling_interval: 5_000,
          query: "#{app_name}.internal",
          node_basename: app_name
        ]
      ]
    ]

REMEMBER: Deploy your updated app so the clustering code is available, with fly deploy.

This configures libcluster to use the DNSPoll strategy and look for other deployed apps using the same $FLY_APP_NAME on the .internal private network.

This assumes that your rel/env.sh.eex file is configured to name your Elixir node using the $FLY_APP_NAME. We did this earlier in the "Naming Your Elixir Node" section.

Before this app can be clustered, we need more than one VM. We'll do that next!

Running Multiple VMs

There are two ways to run multiple VMs.

  1. Scale our application to have multiple VMs in one region.
  2. Add a VM to another region (multiple regions).

Both approaches are valid and our Elixir application doesn't change at all for the approach you choose!

Let's first start with a baseline of our single deployment.

fly status
...
VMs
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
f9014bf7 26      sea    run     running 1 total, 1 passing 0        1h8m ago

Scaling in a Single Region

Let's scale up to 2 VMs in our current region.

fly scale count 2
Count changed to 2

Checking on the status we can see what happened.

fly status
...
VMs
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
eb4119d3 27      sea    run     running 1 total, 1 passing 0        39s ago
f9014bf7 27      sea    run     running 1 total, 1 passing 0        1h13m ago

We now have two VMs in the same region! That was easy.

Let's make sure they are clustered together. We can check the logs:

fly logs
...
app[eb4119d3] sea [info] 21:50:21.924 [info] [libcluster:fly6pn] connected to :"fly-elixir@fdaa:0:1da8:a7b:ac2:f901:4bf7:2"
...

But that's not as rewarding as seeing it from inside a node. From an IEx shell, we can ask the node we're connected to, what other nodes it can see.

fly ssh console
/app/bin/hello_elixir remote
iex(fly-elixir@fdaa:0:1da8:a7b:ac2:f901:4bf7:2)1> Node.list
[:"fly-elixir@fdaa:0:1da8:a7b:ac4:eb41:19d3:2"]

I included the IEx prompt because it shows the IP address of the node I'm connected to. Then getting the Node.list returns the other node. Our two VMs are connected and clustered!

Scaling to Multiple Regions

Fly makes it super easy to run VMs of your applications physically closer to your users. Through the magic of DNS, users are directed to the nearest region where your application is located. You can read about regions here and see the list of regions to choose from.

Starting back from our baseline of a single VM running in sea which is Seattle, Washington (US), I'll add the region ewr which is Parsippany, NJ (US). This puts an VM on both coasts of the US.

fly regions add ewr
Region Pool:
ewr
sea
Backup Region:
iad
lax
sjc
vin

Looking at the status right now shows that we're only in 1 region because our count is set to 1.

fly status
...
VMs
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
cdf6c422 29      sea    run     running 1 total, 1 passing 0        58s ago

Let's add a 2nd VM and see it deploy to ewr.

fly scale count 2
Count changed to 2

Now our status shows we have two VMs spread across 2 regions!

fly status
...
VMs
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
0a8e6666 30      ewr    run     running 1 total, 1 passing 0        16s ago
cdf6c422 30      sea    run     running 1 total, 1 passing 0        6m47s ago

Let's ensure they are clustered together.

fly ssh console
/app/bin/hello_elixir remote
iex(fly-elixir@fdaa:0:1da8:a7b:ac2:cdf6:c422:2)1> Node.list
[:"fly-elixir@fdaa:0:1da8:a7b:ab2:a8e:6666:2"]

We have two VMs of our application deployed to the West and East coasts of the North American continent and they are clustered together! Our users will automatically be directed to the server nearest them. That is so cool!

Troubleshooting

Some problems are harder to diagnose because they deal with Elixir releases or Docker build problems. Typically, you don't run the application that way locally, so you only encounter those problems when it's time to deploy.

Here are a few tips to help diagnose and identify problems.

  • Run mix release locally on your project.
  • Build the Dockerfile locally to verify it builds correctly. docker build .
  • Check the :prod config in config/runtime.exs, which is not used locally. Carefully review it.
  • Run fly logs to check server logs.