Introduction
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
https://fly.io/docs/getting-started/installing-flyctl/
asdf install [erlang, elixir, postgres] [version]
asdf global [erlang, elixir, postgres] [version]
fly version upgrade
brew upgrade gh
Create new Phoenix app
https://hexdocs.pm/phoenix/installation.html
https://hexdocs.pm/phoenix/up_and_running.html
https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html
mix local.hex
mix archive.install hex phx_new
mix phx.new 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
https://fly.io/docs/getting-started/elixir/
fly launch
Setup a custom domain
https://fly.io/docs/app-guides/custom-domains-with-fly/#setting-the-a-and-aaaa-records
https://en.wikipedia.org/wiki/List_of_DNS_record_types
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)
https://hexdocs.pm/phoenix/using_ssl.html#force-ssl
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
https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
fly auth token
gh secret set FLY_API_TOKEN --body [insert auth token here]
# .github/workflows/cicd.yml
name: CICD
on: [push]
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
SECRET_KEY_BASE: [mix phx.gen.secret]
steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: "24.0"
elixir-version: "1.13.2"
- run: mix deps.get
- run: mix test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: superfly/flyctl-actions@1.1
with:
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
https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Auth.html
mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate
Assign current_user to liveviews inside live sessions
https://hexdocs.pm/phoenix_live_view/security-model.html#mounting-considerations
https://hexdocs.pm/phoenix_live_view/security-model.html#live_session-and-live_redirect
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 ->
Accounts.get_user_by_session_token(user_token)
end)
{:cont, socket}
end
end
# 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
end
end
end
We should now have access to socket.assigns.current_user
in our liveviews.
Add todos or whatever
https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Live.html
https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3
https://hexdocs.pm/ecto/Ecto.Schema.html#belongs_to/3
https://hexdocs.pm/phoenix/contexts.html
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
mix phx.gen.live Todos Todo todos title:string done:boolean
# lib/accounts/user.ex
schema :users do
has_many :todos, Todo
end
# lib/todos/todo.ex
schema :todos do
belongs_to :user, User
end
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
https://fly.io/phoenix-files/tailwind-standalone/
https://github.com/phoenixframework/tailwind
From the fly article linked above by Chris McCord:
The phx.new 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).
Conclusion
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