Deploying an Elixir Release using Docker on Fly.io

Deploying an Elixir Release using Docker on Fly.io

Deploy a Phoenix 1.6 app using Elixir Releases and Docker on Fly.io

I'm going to show you how to deploy our Elixir Release to Fly.io. We'll use our Docker image.

Prepare Elixir Release for deploying to Fly.io

Fly.io uses IPv6 in all their internal networks. So we need to configure our app to use IPv6 if we want to connect the app to the database.

Run this command to generate, among others, the rel/env.sh.eex file:

mix release.init

This file runs just before starting our application. It configures environment variables dynamically. Set the contents of the file to this:

#!/bin/sh

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

This file gets the IPv6 assigned by fly.io on startup and assigns it to a variable. Then it uses that variable, along with the FLY_APP_NAME environment variable that fly.io automatically provides, to set another environment variable RELEASE_NODE. This will be used as a unique name for the node that our app is running in. The last line configures the BEAM virtual machine to use IPv6.

Let's modify the config/runtime.exs file.

Change the Saturn.Repo config to:

config :saturn, Saturn.Repo,
    # ssl: true,
    socket_options: [:inet6],
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

Change the SaturnWeb.Endpoint to

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

  config :saturn, SaturnWeb.Endpoint,
    url: [host: "#{app_name}.fly.dev", port: 80],
    http: [
      # Enable IPv6 and bind on all interfaces.
      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
      # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base

Add a .dockerignore file to the root of the project:

assets/node_modules/
deps/

Modify the Dockerfile and change the line that copies the runtime.exs file to this:

# copy runtime configuration file
COPY rel rel
COPY config/runtime.exs config/

I am creating a branch named fly-io-deployment and committing all these changes to it:

git checkout -b fly-io-deployment
git add .
git commit -m "Deploying to fly.io"
git push -u origin fly-io-deployment

Create and configure your Fly.io account

Install flyctl

brew install superfly/tap/flyctl

Sign up to fly.io

If you don't have a fly.io account, create one

flyctl auth signup

Login to fly.io

If you already have a fly.io account, login

flyctl auth login

Create a Fly.io app

Before launching the app ensure you have added a credit card to your organization by visiting fly.io/organizations/personal and adding one. Otherwise, the next command won't work.

Once you're ready, run this command:

fly launch

It will ask you some things to configure your app in fly.io. Leave the App name blank in order to get a random name for it. Pick a region close to where you live and make sure that you answer no to the question about deploying now.

You should see something similar to this:

fly launch
Creating app in /Users/mcoba/Code/saturn
Scanning source code
Detected a Dockerfile app
? App Name (leave blank to use an auto-generated name):
Automatically selected personal organization: Miguel Cobá
? Select region: mad (Madrid, Spain)
Created app damp-paper-3277 in organization personal
Wrote config file fly.toml
? Would you like to deploy now? No
Your app is ready. Deploy with `flyctl deploy`

Open the fly.toml file that flyctl created in the root of the project. Change the kill_signal to:

kill_signal = "SIGTERM"

and add a [deploy] section after [env]

[env]

[deploy]
  release_command = "eval Saturn.Release.migrate"

change the internal_port to:

  internal_port = 4000

Set secrets on Fly.io

We need to create some secrets in Fly.io infrastructure to be used when the app starts.

fly secrets set SECRET_KEY_BASE=$(mix phx.gen.secret)

Create database

Create a database for the app. Aswer the questions leaving the app name blank to get a random name and ensure you select the smallest VM size.

fly postgres create

You should see something similar to this:

fly postgres create

? App Name:
Automatically selected personal organization: Miguel Cobá
? Select region: mad (Madrid, Spain)
? Select VM size: shared-cpu-1x - 256
? Volume size (GB): 10
Creating postgres cluster  in organization personal
Postgres cluster still-sun-6781 created
  Username:    postgres
  Password:   <some big password>
  Hostname:    still-sun-6781.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, 0 healthy, 0 unhealthy [health checks: 6 total, 1 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 1 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 1 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 2 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 3 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 4 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 5 passing,
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 "still-sun-6781.internal."
For example: postgres://postgres:<the big password>@still-sun-6781.internal:5432

See the postgres docs for more information on next steps, managing postgres, connecting from outside fly:  https://fly.io/docs/reference/postgres/

Take note of the generated database name, you'll need it in the next step. Mine is: still-sun-6781.

What remains is to connect the Elixir Release app to the PostgreSQL app. Run this command but use your own database name. This will create a new postgres user and password to connect from the Elixir Release to the PostgreSQL database:

fly postgres attach --postgres-app still-sun-6781

You'll see something like this:

fly postgres attach --postgres-app still-sun-6781

Postgres cluster still-sun-6781 is now attached to damp-paper-3277
The following secret was added to damp-paper-3277:
  DATABASE_URL=postgres://<some new user>:<some new password>@still-sun-6781.internal:5432/damp_paper_3277?sslmode=disable

As you can see, this automatically created a secret with the DATABASE_URL that we were missing.

Deploy to Fly.io

Do the deployment:

fly deploy

This will start the Docker image building, push it to fly.io's registry and then will deploy a container based on that image and will provide the secrets we configure it to start it. After lots of output logs you should see something like this:

==> Release command
Command: eval Saturn.Release.migrate
     Starting instance
     Configuring virtual machine
     Pulling container image
     Unpacking image
     Preparing kernel init
     Configuring firecracker
     Starting virtual machine
     Starting init (commit: 50ffe20)...
     Preparing to run: `bin/saturn eval Saturn.Release.migrate` as elixir
     2021/10/29 23:19:47 listening on [fdaa:0:37f6:a7b:2656:f312:7c7b:2]:22 (DNS: [fdaa::3]:53)
     Reaped child process with pid: 561 and signal: SIGUSR1, core dumped? false
     23:19:50.604 [info] Migrations already up
     Main child exited normally with code: 0
     Reaped child process with pid: 563 and signal: SIGUSR1, core dumped? false
     Starting clean up.
Monitoring Deployment

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

As you see the deployment was executed correctly and it ran the migrations. Now let's visit the app.

fly open

A browser is opened and you should be presented with your app, running on Fly.io infrastructure:

App running on fly.io

Bonus

Connect to the running node with IEx

We need to configure a secure ssh tunnel to the container running in fly.io.

fly ssh establish
fly ssh issue

Answer with your email and select a place to save your private keys. If you already use ssh for other connections you can save it to the same $HOME/.ssh/ directory. I got this:

fly ssh establish
Automatically selected personal organization: Miguel Cobá
Establishing SSH CA cert for organization personal
New organization root certificate:
ssh-ed25519-cert-v01@openssh.com <some big value>

fly ssh issue
? Email address for user to issue cert:  miguel.coba@gmail.com

!!!! WARNING: We're now prompting you to save an SSH private key and certificate       !!!!
!!!! (the private key in "id_whatever" and the certificate in "id_whatever-cert.pub"). !!!!
!!!! These SSH credentials are time-limited and handling them in files is clunky;      !!!!
!!!! consider running an SSH agent and running this command with --agent. Things       !!!!
!!!! should just sort of work like magic if you do.                                    !!!!
? Path to store private key:  ~/.ssh/id_fly_io
? Path to store private key:  /Users/mcoba/.ssh/.id_fly_io
Wrote 24-hour SSH credential to /Users/mcoba/.ssh/.id_fly_io, /Users/mcoba/.ssh/.id_fly_io-cert.pub

You can now connect to the container with fly ssh console and connect to the erlang node with app/bin/saturn remote:

fly ssh console
Connecting to damp-paper-3277.internal... complete
/ # cd ~
/home/elixir # ls
app
/home/elixir # app/bin/saturn remote
Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [jit:no-native-stack]

Interactive Elixir (1.12.3) - press Ctrl+C to exit (type h() ENTER for help)

That's it.

Source code

The source code for the saturn project is open source under the MIT license. Use the fly-io-deployment branch.

About

I'm Miguel Cobá. I write about Elixir, Elm, Software Development, and eBook writing.

Photo by Jan Ranft on Unsplash