Deploying an Elixir Release using Docker on Fly.io
Tags: elixir, phoenixframework, docker, webdev
Published: 2021-11-01 12:27:56

---
title: Deploying an Elixir Release using Docker on Fly.io
published: true
description: Deploy an Elixir Release to Fly.io with Docker
tags: elixir, phoenixframework, docker, webdevelopment
//cover_image: https://direct_url_to_image.jpg
canonical_url: https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-flyio
series: Deploying Elixir
---

I'm going to show you how to deploy our Elixir Release to Fly.io. We'll use our  [Docker image](https://blog.miguelcoba.com/deploying-a-phoenix-16-app-with-docker-and-elixir-releases).

## 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:

```bash
mix release.init
```

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

```bash
#!/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:

```elixir
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

```elixir
  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:

```Dockerfile
# 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:

```bash
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

```bash
brew install superfly/tap/flyctl
```

### Sign up to fly.io

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

```bash
flyctl auth signup
```

### Login to fly.io

If you already have a fly.io account, login

```bash
flyctl auth login
```

## 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:

```bash
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:

```bash
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:

```toml
kill_signal = "SIGTERM"
```

and add a `[deploy]` section after `[env]`

```toml
[env]

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

change the `internal_port` to:

```toml
  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.

```bash
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.

```bash
fly postgres create
```

You should see something similar to this:

```bash
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:

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

You'll see something like this:

```bash
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:

```bash
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:

```bash
==> 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.

```bash
fly open
```

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


![App running on fly.io infrastructure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mceqz5djwfvxkasb38sc.png)
 

## Bonus

### Connect to the running node with IEx

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

```bash
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:

```bash
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`:

```bash
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](https://github.com/miguelcoba/saturn/tree/fly-io-deployment)  for the saturn project is open source under the MIT license. Use the `fly-io-deployment` branch.

## About

I'm [Miguel Cobá](https://miguelcoba.com). I write about Elixir, Elm, Software Development, and eBook writing.

- Follow me on [Twitter](https://twitter.com/MiguelCoba_)
- Subscribe to my [newsletter](https://newsletter.miguelcoba.com)
- Read all my articles on my [blog](https://blog.miguelcoba.com)
- Get my books:
    - [100 Elixir Tips](https://store.miguelcoba.com/l/100elixirtips) [FREE]
    - [Deploying Elixir](https://store.miguelcoba.com/l/deployingelixir) [FREE]
    - [Deploying Elixir: Advanced Topics](https://store.miguelcoba.com/l/advancedtopics)
    - [eBook Writing Workflow for Developers](https://store.miguelcoba.com/l/ebookwriting)