DEV Community

Miguel Cobá
Miguel Cobá

Posted on • Updated on • Originally published at blog.miguelcoba.com

Deploying an Elixir Release using 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

assets/node_modules/
deps/
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create and configure your Fly.io account

Install flyctl

brew install superfly/tap/flyctl
Enter fullscreen mode Exit fullscreen mode

Sign up to fly.io

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

flyctl auth signup
Enter fullscreen mode Exit fullscreen mode

Login to fly.io

If you already have a fly.io account, login

flyctl auth login
Enter fullscreen mode Exit fullscreen mode

Create a Fly.io app

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

Once you're ready, run this command:

fly launch
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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

kill_signal = "SIGTERM"
Enter fullscreen mode Exit fullscreen mode

and add a [deploy] section after [env]

[env]

[deploy]
  release_command = "eval Saturn.Release.migrate"
Enter fullscreen mode Exit fullscreen mode

change the internal_port to:

  internal_port = 4000
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

fly open
Enter fullscreen mode Exit fullscreen mode

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

App running on fly.io infrastructure

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (4)

Collapse
 
idcmardelplata profile image
Martin Algañaraz

I have followed all the steps and several guides but it always throws me the same error "** (RuntimeError) FLY_APP_NAME not avaiable" I am trying to deploy an umbrella application so I have configured some things differently but I suppose it should not exist problems with the FLY_APP_NAME environment variable since from what I've read it is automatically generated by fly. Could you guide me on what may be happening? Thank you very much in advance

Collapse
 
brainlid profile image
Mark Ericksen

Hey Martin!

Yes, umbrella projects work on Fly. I like to build projects that way myself. I'd suggest directing questions to community.fly.io/. Perhaps there you could share more context about your setup and the error message?

Collapse
 
miguelcoba profile image
Miguel Cobá

Thanks, Mark! Good to know!

Collapse
 
miguelcoba profile image
Miguel Cobá

Hi, Martín. I have not used umbrellas so no idea if something different must be done. There are two things that I think you can try:

  • fly secrets list, to see if the FLY_APP_NAME exists
  • set the env var locally (export FLY_APP_NAME=something) and then start the umbrella locally in prod mode and see if the same error happens
  • check if you need to set the REPLACE_OS_VARS (cultivatehq.com/posts/elixir-disti...)

Hope that helps!