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:
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.
- Follow me on Twitter
- Subscribe to my newsletter
- Read all my articles on my blog
- Get my books: