Introduction
This is an opinionated link-first getting started guide. It is my preferred setup as of last authoring, and is liable to change over time.
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] [version]
asdf global [erlang, elixir] [version]
brew upgrade gh
brew upgrade flyctl
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
Deploy on Fly
https://fly.io/docs/getting-started/elixir/
fly launch
This command will take you all the way to cloud deployment on a fly.dev
URL.
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]
gh secret set SECRET_KEY_BASE --body $(mix phx.gen.secret)
# .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: ${{ secrets.SECRET_KEY_BASE }}
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
This code does a lot. I encourage you to familiarize yourself with it over time.
Auth for liveview
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 particularly noteworthy part of the generated code is that it has wired up the authentication for liveviews. Look in router.ex
and notice how some of the live
routes are wrapped in live_session
calls with defined on_mount
functions. Those on_mount
functions are defined in user_auth.ex
. on_mount
is a lifecycle method for liveview connections. It is called both in the initial html request and during initial socket upgrade. The generated code is assigning socket.current_user
when possible, and running a few other contextual checks.
The short version of this is that 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 similar across many applications. Your liveviews enable user interactions. Those interactions a capture in liveview actions and are passed to contexts. You can pass the current_user
in to your contexts as appropriate (if only they can access data, for example). This can all be thoroughly tested via the generated fixture classes in generators.
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