We’re going to go through the steps to deploy Elixir and Phoenix to Fly with Continuous Deployment using GitHub Actions. Fly was awarded Best for LiveView apps in our article covering the Best Cloud Hosting Providers for Elixir and Phoenix.
The cool thing about Fly is there is only one step. Its deployment experience is truly amazing. As you’ll see throughout the book, the Fly command-line tool is also very helpful for a variety of tasks. I’ll show you how easy it is to deploy your app and then take a minute to review the files generated during the deployment process.
If your app uses LiveView, we’ll give you a starting point to deploy your Phoenix Elixir app to Fly using Continuous Deployment with GitHub Actions.
Note: This post has been updated using Chapter 1 and 2 of the Phoenix Deployment Handbook.
Overview
Here are the topics we’ll cover while deploying the project.
- Install Fly CLI tool and sign up
- Create a new Phoenix LiveView app
- Deploy app on Fly
- Review the files generated by Fly
- Checking the status of your app
- Continuous Deployment with GitHub Actions
- What is Continuous Integration / Continuous Deployment (CI/CD)?
- Add GitHub Actions workflow file
- GitHub Secrets
- Run GitHub Actions workflow
Prerequisites
To follow this guide, make sure you have at least the following applications installed on your machine. In the brackets are the versions with which this guide was written. This guide takes advantage of the latest features in these versions.
- Elixir (1.13.1-otp-24)
- Erlang (OTP 24.2)
- Phoenix (1.6.6)
If you run into problems, the finished project is available on GitHub as StakNine HelloDeploy.
Install Fly CLI tool and sign up
We’re going to show you how easy it is to deploy with Fly. First, you need to install the Flyctl and log in.
We’re using macOS, check out the Installing flyctl guide for other operating systems.
$ brew install superfly/tap/flyctl
Sign up and then login with flyctl
:
$ flyctl auth signup
$ flyctl auth login
Great! We’re all set to deploy to Fly.
Create a new Phoenix LiveView app
We are going to create a simple Elixir Phoenix LiveView project called HelloDeploy
.
Use the command line to update your Phoenix version (you need at least v1.6.6) and create a new Phoenix project.
$ mix archive.install hex phx_new
$ mix phx.new hello_deploy --live
With Fly, it is easier to deploy your app to production than it is to start your server locally. To demonstrate, we’re going to deploy to production first then run our app locally with releases.
Therefore, you can ignore the instructions to create the database and start your server manually after Phoenix completes generating your project.
Change into the project directory and commit the generated project. We want to commit these files now so we can see what Fly generates for you during deployment.
$ cd hello_deploy
$ git init
$ git add .
$ git commit -m 'initialize repo'
Now that we have the Fly CLI tool set up and our project files generated, we’re ready to deploy.
Deploy app on Fly
When we run fly launch
from the newly-created project directory, the launcher will:
- Ask you to select a deployment region
- Set secrets required by Phoenix (
SECRET_KEY_BASE
, for example) - Run the Phoenix deployment setup task
- Optionally setup a Postgresql database in your selected region
- Deploy the application in your selected region
Alright, let’s deploy with fly launch
!
$ fly launch
Creating app in /workspace/staknine/hello_deploy
Scanning source code
Detected a Phoenix app
? App Name (leave blank to use an auto-generated name):
Automatically selected personal organization: StakNine
? Select region: lax (Los Angeles, California (US))
Created app nameless-river-4364 in organization personal
Set secrets on nameless-river-4364: SECRET_KEY_BASE
Preparing system for Elixir builds
Installing application dependencies
Running Docker release generator
Wrote config file fly.toml
? Would you like to setup a Postgres database now? Yes
Postgres cluster nameless-river-4364-db created
...
? Would you like to deploy now? Yes
Deploying nameless-river-4364
...
That’s it!
Here are a few fly commands you can use to manage your app:
fly open
– Open your app in the browserfly logs
– Tail your application logsfly status
– App deployment detailsfly deploy
– Deploy the application after making changes
Run fly open
to see your deployed app in action.
Review the files generated by Fly
Before we move on to the next section, let’s take a minute to review the files generated by Fly during the deployment process. Let’s run git status
and see what new files are in our repo.
$ git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
.dockerignore
Dockerfile
fly.toml
lib/hello_deploy/release.ex
rel/
mix phx.gen.release
Fly uses the mix phx.gen.release --docker
command from Phoenix to generate release files and an optional Dockerfile for release-based deployments.
The following release files are created:
lib/app_name/release.ex
– A release module containing tasks for running migrations inside a releaserel/overlays/bin/migrate
– A migrate script for conveniently invoking the release system migrationsrel/overlays/bin/server
– A server script for conveniently invoking the release system with environment variables to start the phoenix web server Note, therel/overlays
directory is copied into the release build by default when runningmix release
. It also uses the new--docker
flag which generated:Dockerfile
– The Dockerfile for use in any standard docker deployment.dockerignore
– A docker ignore file with standard elixir defaults
fly.toml
Fly also added configuration to a fly.toml
file. It contains a default configuration for deploying your Phoenix app.
# fly.toml file generated for nameless-river-4364 on 2022-01-08T19:22:05-08:00
app = "nameless-river-4364"
kill_signal = "SIGTERM"
kill_timeout = 5
processes = []
[deploy]
release_command = "/app/bin/migrate"
[env]
PHX_HOST = "nameless-river-4364.fly.dev"
PORT = "8080"
[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 main thing to point out is the release command
in the [deploy]
setting. It tells Fly on each new deployment, to run our database migrations using the HelloDeploy.Release
module added by mix phx.gen.release
.
Checking the status of your app
Before we go, let’s check the status of our app from the command line using fly status
. The Fly CLI will allow you to accomplish almost everything you need from the command line.
You’ll get information about your App including the Hostname (URL) for your app. You’ll also get the status of your latest deployment. Finally, the command will list all active instances of your app.
$ fly status
App
Name = nameless-river-4364
Owner = personal
Version = 0
Status = running
Hostname = nameless-river-4364.fly.dev
Deployment Status
ID = 3cd84555-3bf7-14bb-7b5c-24654bdb4647
Version = v0
Status = successful
Description = Deployment completed successfully
Instances = 1 desired, 1 placed, 1 healthy, 0 unhealthy
Instances
ID PROCESS VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
3f0d78f3 app 0 lax run running 1 total, 1 passing 0 1h52m ago
In addition, you can check the status of your app from the Fly dashboard. You can open your dashboard with the fly dashboard
CLI command.
You created your Phoenix app and deployed it to production with one command. Fly delivers an amazing developer experience and continues to release additional features all the time.
Next, we’ll use the content from Chapter 2 of the Phoenix Deployment Handbook to learn how to deploy automatically when you merge changes and push them to GitHub.
Continuous Deployment with GitHub Actions
Now we are going to take a few minutes to set up a complete Continuous Integration / Continuous Deployment (CI/CD) pipeline using GitHub Actions. The two goals of the CI/CD pipeline are to run tests when you open a new pull request on GitHub and automatically deploy any changes merged into your main
branch. With the pipeline, you will reduce the number of bugs you push into production. You’ll also save time with the deployment process since it will be completely automated. It will even automate your database migrations since your fly.toml
includes migrations in its release command.
What is Continuous Integration / Continuous Deployment (CI/CD)?
According to Red Hat, the "CI" in CI/CD always refers to continuous integration, which is an automation process for developers. Successful CI means new code changes to an app are regularly built, tested, and merged to a shared repository.
The "CD" in CI/CD refers to continuous delivery and/or continuous deployment.
Continuous delivery usually means a developer’s changes to an application are automatically bug tested and uploaded to a repository (like GitHub), where they can then be deployed to a live production environment by the operations team.
Continuous deployment (the other possible "CD") can refer to automatically releasing a developer’s changes from the repository to production.
Based on these definitions, we’re going to set up a continuous integration / continuous deployment pipeline. We’ll use GitHub Actions which is a feature built into your GitHub repository.
Add GitHub Actions workflow file
GitHub Actions allow you to start with a preconfigured workflow template. However, we’re going to use our own template which combines the Elixir template from GitHub and a template Fly provides for deploying with GitHub Actions.
First, create a new repo on GitHub and push your existing repository from the command line. Now that your project is on GitHub, we can set up GitHub Actions.
My initial experience learning programming was heavily influenced by Upcase lessons from Thoughtbot. While it was mostly focused on Ruby on Rails, I still use their git workflow guide to add features using branches and pull requests.
I find it helpful to review my code on GitHub outside of my editor. It helps me look at it from a different perspective and spot things I missed in my editor.
We’ll roughly follow the Thoughtbot git workflow guide for the remainder of the article.
Next, check out a new git branch.
$ git checkout -b action
With a git branch, you can make commits in the branch. Then you merge them into the main
branch when you are complete with the bug fix or feature.
After you are in a new branch called action
, create .github/workflows/main.yml
with these contents. Here is the file on GitHub to make it easier to copy over.
name: Fly CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: Build and test
runs-on: ubuntu-latest
services:
db:
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
image: postgres:11
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/setup-elixir@885971a72ed1f9240973bd92ab57af8c1aa68f24
with:
elixir-version: '1.13.1' # Define the elixir version [required]
otp-version: '24.2' # Define the OTP version [required]
- name: Restore dependencies cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Run tests
run: mix test
deploy:
name: Deploy app
needs: test
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: superfly/[email protected]
with:
args: "deploy"
Let’s review the content of the file. First, we’ll use the default settings to run our workflow for both pull requests and pushes to main
.
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
Next, we have two jobs. We’ve named the first job Build and test
Continuous Integration
Our first job will accomplish the following continuous integration tasks:
- Set up a test database
- Install dependencies
- Run mix test
jobs:
test:
name: Build and test
runs-on: ubuntu-latest
services:
db:
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
image: postgres:11
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/setup-elixir@885971a72ed1f9240973bd92ab57af8c1aa68f24
with:
elixir-version: '1.13.1' # Define the elixir version [required]
otp-version: '24.2' # Define the OTP version [required]
- name: Restore dependencies cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Run tests
run: mix test
You can add other commands after you run mix test
. For example, you could check formatting with mix format --check-formatted
or run static code analysis with a tool like Credo.
Continuous Deployment
Our second job, named Deploy app
, will accomplish the following continuous deployment tasks:
- Get
FLY_API_TOKEN
from GitHub secrets - Use
superfly/flyctl-actions
action to deploy on Fly
deploy:
name: Deploy app
needs: test
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: superfly/[email protected]
with:
args: "deploy"
superfly/flyctl-actions
is a GitHub action created by Fly which wraps the flyctl
command. The wrapper is invoked with the deploy
argument which will take over the process of building and moving the application to the Fly infrastructure.
It uses the settings from the fly.toml
file to guide it and uses the FLY_API_TOKEN
to authorize its access to the Fly API.
There is one change to the Fly template. We’ve added the following line to only run the Deploy app
job when we push a commit to main. We don’t want to deploy when we push commits to branches on GitHub.
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
For more details about the deploy step, check out Continuous Deployment with Fly and GitHub Actions.
GitHub Secrets
Next, we will use GitHub secrets to create and store an encrypted secret to use in the workflow for our Fly API token.
First, you need to get a Fly API token with:
$ flyctl auth token
Follow the instructions for creating and storing encrypted secrets to store the auth token value as FLY_API_TOKEN
in your repository secrets.
You’ll accomplish this in the Settings tab of your GitHub repo.
Once you’ve stored your FLY_API_TOKEN
, let’s confirm our tests pass locally.
$ mix test
...
Finished in 0.02 seconds (0.02s async, 0.00s sync)
3 tests, 0 failures
Then push our changes to GitHub.
$ git add .
$ git commit -m "add workflow file for GitHub Actions"
$ git push origin action
Finally, open a pull request on GitHub.
Run GitHub Actions workflow
Now go to the Actions tab to see the progress and results of your workflow. Your Build and test
job should run the tests and pass.
The Deploy app
job should not run since it only runs on pushes to main.
Now that we’ve checked only the tests run on a pull request, merge your changes with the main branch in your git repository and push to origin.
$ git checkout main
$ git merge action --ff-only
$ git push
This time both the Build and test
and Deploy app
jobs should run successfully.
Finally, use fly status
or visit your app in the browser with fly open
to check your app is deployed properly.
Let’s wrap up by deleting our remote and local git branches.
$ git push origin --delete main
$ git branch --delete main
You are all set up with a CI/CD pipeline that runs tests for each pull request and automatically deploys each new commit. You may have steps to add as your project progresses, but this simple initial setup will reduce bugs in production and speed up the delivery of features to your users.
Conclusion
Congratulations! You went through the steps to deploy your Phoenix Elixir app to Fly. Then you built a CI/CD pipeline with GitHub Actions to automatically run your automated tests on each pull request and automate deployment to Fly when you push commits to your default (e.g. main) branch. Now that you’ve deployed to production, check out our article covering the best error monitoring services for Elixirso you can deploy with confidence and effectively manage errors in your system.
This post is adopted from Chapter 1 and 2 of the Phoenix Deployment Handbook. Check it out below if you want to streamline your entire deployment process!