Getting Started with Elixir Phoenix on Fly

Image of Author
February 1, 2022 (last updated October 5, 2022)


This is an opinionated link-first getting started guide. It is my preferred setup as of last authoring, and as such is also liable to change as I change tooling opinions over time, etc.

This guide will walk through setting up an Elixir Phoenix Liveview full-stack app deployed on Fly on a custom domain. The codebase will include/use PostgreSQL, TailwindCSS, Github Actions, Github CLI, and asdf.

Below I will use g as an alias for git, and g cm as an alias for git commit --message.

Prepare machine

asdf install [erlang, elixir, postgres] [version]
asdf global [erlang, elixir, postgres] [version]
fly version upgrade
brew upgrade gh

Create new Phoenix app

mix local.hex
mix archive.install hex phx_new
mix app
cd app
mix ecto.create

Setup Github repo

g init
gh repo create app --public --source=.
g add .
g cm "First commit."
g push -u origin main

g = git and g cm = git commit --message

Deploy on Fly

fly launch

Setup a custom domain

fly ips list

Use the listed IPs to update your DNS resource. IPv4 uses A records, IPv6 uses AAAA records. It will look something like:

[custom domain name] | A | 1 hour | 111.222.333.444
[custom domain name] | AAAA | 1 hour | 1111:2222:3::4:5555
fly certs create [custom domain name]

Enforce TLS (redirect http -> https)

From the documentation above, host: nil enables dynamic https redirects based on the origin of the request. So, this means that https will be enforced on both the fly domain and the custom domain.

# config/prod.exs

config :app, App.Endpoint,
  force_ssl: [
    hsts: true,
    host: nil,
    rewrite_on: [:x_forwarded_proto]

Create CICD pipeline with Github Actions

fly auth token
gh secret set FLY_API_TOKEN --body [insert auth token here]
# .github/workflows/cicd.yml

name: CICD
on: [push]
  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
    runs-on: ubuntu-latest
        image: postgres
          POSTGRES_PASSWORD: postgres
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      SECRET_KEY_BASE: [mix phx.gen.secret]
      - uses: actions/checkout@v2
      - uses: erlef/setup-beam@v1
          otp-version: "24.0"
          elixir-version: "1.13.2"
      - run: mix deps.get
      - run: mix test

    needs: test
    runs-on: ubuntu-latest
      - uses: actions/checkout@v2
      - uses: superfly/flyctl-actions@1.1
          args: "deploy"

A public repo might want to store the SECRET_KEY_BASE as an env var instead of hard-coding it in the repo. The postgres service is port-mapped to localhost:5432 with appropriate credentials as per config/test.exs, which is why there's no need for DATABASE_URL in the env (at least for now).

Add authentication, authorization, and users

mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate

Assign current_user to liveviews inside live sessions

One good way to keep track of the current user is to wrap our liveview routes in a live_session with the on_mount option pointing to a custom UserAuthLive module (or whatever you want to call it). This allows us to pass a current_user to our liveviews contained within. We are using the previously generated phx.gen.auth code for a lot of the heavy lifting here.

# app_web/controllers/user_auth_live.ex

defmodule AppWeb.UserAuthLive do
  import Phoenix.LiveView
  alias App.Accounts

  def on_mount(:default, _params, %{"user_token" => user_token} = _session, socket) do
    socket =
      assign_new(socket, :current_user, fn ->

    {:cont, socket}
# lib/app_web/router.ex

defmodule AppWeb.Router do
  scope "/", AppWeb do
    pipe_through :browser

    live_session :user_auth, on_mount: AppWeb.UserAuthLive do
      pipe_through :require_authenticated_user

      # live routes here

We should now have access to socket.assigns.current_user in our liveviews.

Add todos or whatever

mix Todos Todo todos title:string done:boolean
# lib/accounts/user.ex
schema :users do
  has_many :todos, Todo

# lib/todos/todo.ex
schema :todos do
  belongs_to :user, User

Whatever contexts and liveviews you generate, the flow of logic will likely be something like: Your liveviews call your context functions and provide the current_user as appropriate. This can all be thoroughly tested via the generated fixture classes in generators.

Add TailwindCSS and remove generated html and css

From the fly article linked above by Chris McCord:

The project generator will add Tailwind support in a future release

This is so likely to change, and will have such a large impact on generators, that I'm not going to elaborate on this step.

Next Steps

Email delivery solution (with Magic Links), OAuth with Social Sign-On (SSO).


At this point you should have a full-stack Elixir Phoenix LiveView app deployed to Fly on a custom domain.

Thank you for reading :D