Deploy Elixir Phoenix to Fly with ONE command then set up Continuous Deployment

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.

  1. Install Fly CLI tool and sign up
  2. Create a new Phoenix LiveView app
  3. Deploy app on Fly
  4. Review the files generated by Fly
  5. Checking the status of your app
  6. Continuous Deployment with GitHub Actions
  7. What is Continuous Integration / Continuous Deployment (CI/CD)?
  8. Add GitHub Actions workflow file
  9. GitHub Secrets
  10. 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 browser
  • fly logs – Tail your application logs
  • fly status – App deployment details
  • fly deploy – Deploy the application after making changes

Run fly open to see your deployed app in action.

fly phoenix homepage

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 release
  • rel/overlays/bin/migrate – A migrate script for conveniently invoking the release system migrations
  • rel/overlays/bin/server – A server script for conveniently invoking the release system with environment variables to start the phoenix web server Note, the rel/overlays directory is copied into the release build by default when running mix 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.

fly dashboard

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.

continuous integration github actions

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.

continuous deployment github actions

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!

Want To Know How To Deploy Phoenix Apps Using A single Command?

This brand-new FREE training reveals the most powerful new way to reduce your deployment time and skyrocket your productivity… and truly see your programming career explode off the charts!