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 calledentry
. This uses that symlink run the migrate command. - The
kill_signal
is set toSIGTERM
. An Elixir node does a clean shutdown when it receives aSIGTERM
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