Legacy pre v1.6.3 Phoenix App

Getting an application running on Fly.io 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.io platform.

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

This guide is for applications generated on Phoenix versions older than v1.6.3.

If you’re on 1.6.3 or higher, deployment setup is baked into the framework! Check out our guide for launching newer Elixir apps.

Running the Application Locally


Let’s walk through working with Phoenix locally and preparing it for deployment. We can start with a brand new Phoenix application.

mix phx.new hello_elixir

Tip: Make sure you have a local PostgreSQL server installed and running.

Now, we setup the database and start the application.

mix ecto.setup
The database for HelloElixir.Repo has been created
mix phx.server
[info] Running HelloElixirWeb.Endpoint with cowboy 2.8.0 at 0.0.0.0:4000 (http)
[info] Access HelloElixirWeb.Endpoint at http://localhost:4000

Connect to localhost:4000 to confirm that you have a working Elixir application.

Generate Release Config Files


We use the mix release.init command to create some sample files in the ./rel directory.

mix release.init
* creating rel/vm.args.eex
* creating rel/remote.vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex

Special Note on Fly Networking


Internally Fly uses IPv6 networking. This enables some cool features, but legacy Elixir applications need to be configured to work smoothly with it. These config changes tell Elixir, Phoenix, and the BEAM that we are using IPv6 addresses. The options look like inet6 and inet6_tcp. The next steps are how we configure that in our application.

We only need to configure rel/env.sh.eex. This file gets turned into a shell script that the release uses to set ENV values used when we run any release commands. Here’s the important parts.

#!/bin/sh

ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
export RELEASE_DISTRIBUTION=name
export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp"

We configure the node to use a full node name when it runs. We get the Fly assigned IPv6 address and use that to name our node. Finally, we configure inet6_tcp for the BEAM as well.

Even if you don’t care to cluster your nodes together, you still want to do this because it enables running an IEx shell in a running node.

Docker Setup


Dockerfile


You can find a good Dockerfile ready to build your application in the hello_elixir GitHub repo. It is important to note that it uses a Debian-slim base image to build. This avoids DNS issues in Alpine images. The base elixir image is maintained by the hexpm org and is kept up to date. Hex.pm is the official package repository for Elixir. There are other options instead of Debian there as well if you prefer.

The Dockerfile uses a two-stage approach. There are two FROM commands. The first stage pulls in the source and builds the release. The second stage takes the prepared release and sets it up in a minimal Docker image. The final deployed image contains only our release.

Docker Ignore File


We add the file .dockerignore to the project with the following contents. Depending on how you COPY things into the Dockerfile, you may or may not need to configure this.

assets/node_modules/
deps/

This ensures we keep any natively compiled Elixir or Node packages from our development environments from causing a problem in the Linux container.

Launch the App on Fly


To launch an app on fly, run fly launch in the directory with your source code. This creates and configures a fly app for you by inspecting your source code, then prompts you to deploy.

fly launch
$ fly launch
Scanning source code
Detected Dockerfile app
? Select region: sea (Seattle, Washington (US))
Created app fly-elixir in organization personal
Wrote config file fly.toml
? Would you like to deploy now? No

Don’t deploy it just yet. We’re going to adjust the generated fly.toml file first.

The fly launch command scans your source code to determine how to build a deployment image as well as identify any other configuration your app needs, such as secrets and exposed ports.

After your source code is scanned and the results are printed, you’ll be prompted for an organization. Organizations are a way of sharing applications and resources between Fly users. Every Fly account has a personal organization, called personal, which is only visible to your account. Let’s select that for this guide.

Next, you’ll be prompted to select a region to deploy in. The closest region to you is selected by default. You can use this or change to another region. You can find the list of supported regions here.

At this point, flyctl created an app for you and wrote your configuration to a fly.toml file. You’ll then be prompted to build and deploy your app. Once complete, your app will be running on fly.

Inside fly.toml


The fly.toml file now contains a default configuration for deploying your app. In the process of creating that file, flyctl has also generated a Fly-side application slot with a new name. In this case, it is fly-elixir. If we look at the fly.toml file we can see the name in there:

app = "fly-elixir"

kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

The flyctl command will always refer to this file in the current directory if it exists, specifically for the app name/value at the start. That name is used to identify the application to the Fly platform. The rest of the file contains settings to be applied to the application when it deploys.

We’ll have more details about these properties as we progress, but for now, it’s enough to say that they mostly configure which ports the application will be visible on.

Customizing fly.toml


Elixir applications need a little customization to the generated fly.toml file.

app = "fly-elixir"

kill_signal = "SIGTERM"
kill_timeout = 5
processes = []

[deploy]
  release_command = "/app/entry eval HelloElixir.Release.migrate"

[env]

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 4000
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "30s"  # allow some time for startup
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

There are two important changes here:

  • We added the [deploy] setting. This tells Fly that on a new deploy, run our database migrations. The Dockerfile we’re using creates a symlink to your app name called entry. This uses that symlink run the migrate command.
  • The kill_signal is set to SIGTERM. An Elixir node does a clean shutdown when it receives a SIGTERM from the OS.
  • The internal_port is set to 4000 for our Elixir application. The port value should match your application.

Some other values were tweaked as well.

Preparing to Deploy


We’re almost there! Before we can deploy our new app, we need to setup a few things in our Fly account first. Namely, we want to provide the needed secrets and we need a database!

Setting our Secrets on Fly


Elixir has a mix task that can generate a new Phoenix key base secret. Let’s use that.

mix phx.gen.secret

It generates a long string of random text. Let’s store that as a secret for our app. When we run this command in our project folder, flyctl uses the fly.toml file to know which app we are setting the value on.

fly secrets set SECRET_KEY_BASE=<GENERATED>

Creating our Fly Postgres Database


fly postgres create
? App name: hello-elixir-db
Automatically selected personal organization: Mark Ericksen
? Select region: sea (Seattle, Washington (US))
? Select VM size: shared-cpu-1x - 256
? Volume size (GB): 10
Creating postgres cluster hello-elixir-db in organization personal
Postgres cluster hello-elixir-db created
  Username:    <USER>
  Password:    <PASSWORD>
  Hostname:    hello-elixir-db.internal
  Proxy Port:  5432
  PG Port: 5433
Save your credentials in a secure place, you won't be able to see them again!

Monitoring Deployment

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

Connect to postgres
Any app within the personal organization can connect to postgres using the above credentials and the hostname "hello-elixir-db.internal."
For example: postgres://<USER>:<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/

We can take the defaults which select the lowest values for CPU, size, etc. This is perfect for getting started.

Attach our App to the Database


We use flyctl to attach our app to the database which also sets our needed DATABASE_URL ENV value.

fly postgres attach hello-elixir-db
Postgres cluster hello-elixir-db is now attached to fly-elixir
The following secret was added to fly-elixir:
  DATABASE_URL=postgres://<NEW_USER>:<NEW_PASSWORD>@hello-elixir-db.internal:5432/fly_elixir?sslmode=disable

We can see the secrets that Fly is using for our app like this.

fly secrets list
NAME            DIGEST                           DATE
DATABASE_URL    830d8769ff33cba6c8b29d1cd6a6fbac 1m10s ago
SECRET_KEY_BASE 84c992ac7ef334c21f2aaecd41c43666 9m20s ago

Looks like we’re ready to deploy!

Deploying to Fly


To deploy your app, just run just run:

fly deploy

First, flyctl builds our Dockerfile and pushes it to a Fly container registry.

This will lookup our fly.toml file, and get the app name fly-elixir from there. Then flyctl will start the process of deploying our application to the Fly platform. Flyctl returns you to the command line when it’s done.

Viewing the Deployed App


Now that the application has been deployed, let’s find out more about its deployment. The command fly status will give you all the essential details.

fly status
App
  Name     = fly-elixir
  Owner    = personal
  Version  = 3
  Status   = running
  Hostname = fly-elixir.fly.dev

Deployment Status
  ID          = 9762642f-baa4-e4df-c683-13f2ce26a6bc
  Version     = v3
  Status      = successful
  Description = Deployment completed successfully
  Instances   = 1 desired, 1 placed, 1 healthy, 0 unhealthy

Instances
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
f617e72a 3       sea    run     running 1 total, 1 passing 0        1m34s ago

Connecting to the App


The quickest way to browse your newly deployed application is with the fly apps open command.

fly apps open
Opening https://fly-elixir.fly.dev/

Your browser will be sent to the displayed URL. Fly will auto-upgrade this URL to a HTTPS secured URL.

Special Note on Clustering


To make clustering your Elixir applications easier on Fly, in the env.sh.eex file, the RELEASE_NODE is named using the $FLY_APP_NAME and the IPv6 address. It will look something like this in practice.

fly-elixir@fdaa:0:1da8:a7b:ac2:216b:da3f:2

Runtime Config


When we created the app, the file config/runtime.exs was created for us. Now we need to update it.

import Config

if config_env() == :prod do
  database_url =
    System.get_env("DATABASE_URL") ||
      raise """
      environment variable DATABASE_URL is missing.
      For example: ecto://USER:PASS@HOST/DATABASE
      """

  config :hello_elixir, HelloElixir.Repo,
    # IMPORTANT: Or it won't find the DB server
    socket_options: [:inet6],
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

  secret_key_base =
    System.get_env("SECRET_KEY_BASE") ||
      raise """
      environment variable SECRET_KEY_BASE is missing.
      You can generate one by calling: mix phx.gen.secret
      """

  # IMPORTANT: Get the app_name we're using
  app_name =
    System.get_env("FLY_APP_NAME") ||
      raise "FLY_APP_NAME not available"

  config :hello_elixir, HelloElixirWeb.Endpoint,
    # IMPORTANT: tell our app about the host name to use when generating URLs
    url: [host: "#{app_name}.fly.dev", port: 80],
    http: [
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base

  # IMPORTANT: Enable the endpoint for releases
  config :hello_elixir, HelloElixirWeb.Endpoint, server: true
end

This code expects to receive some ENV values at runtime. We’re expecting SECRET_KEY_BASE with our Phoenix secret, our FLY_APP_NAME from Fly, and the DATABASE_URL for connecting to a Fly hosted Postgres database.

Also, you don’t need to turn on TLS for connecting to the Postgres instance. Fly private networks operate over an encrypted WireGuard mesh, so traffic between application servers and PostgreSQL is already encrypted and there’s no need to TLS.

Bonus Sections


With your application up and running, there are some additional things you can do to go further. Using some flyctl commands, we can easily do some powerful things with our application.

These bonus tips cover:

  • Getting an IEx shell into your running node. This helps you manage and work with your running system.
  • Clustering multiple Elixir nodes together. Say “Hello!” to the power of Distributed Computing!
  • Scaling your application out to more machines and even distant regions (with or without clustering).

What is the IP Address?


If you want to know what IP addresses the app is using, try fly ips list:

fly ips list
TYPE ADDRESS                              CREATED AT
v4   213.188.199.124                      24m5s ago
v6   2a09:8280:1:ce56:c80f:5071:6e94:6688 24m5s ago