Deploying Elixir to Render.com

I've deployed a few different Elixir services to the hosting platform https://render.com and found their provided documentation to be lacking for real-world Phoenix apps. Especially if you're using Live View. Here's an augmentation to their guide with the extra steps you might be missing.

Initial Setup

Follow the official guide: https://render.com/docs/deploy-phoenix but don't deploy your service yet. You'll need to make some changes:

Add Evars

Add some new evars:

  • ELIXIR_VERSION = 1.10.3 or whatever version you like. But the default version is older and breaks the build for new Elixir apps
  • DATABASE_URL = The connection string for the database you'll be using

Migrations

After, do these things. MAKE SURE YOU REPLACE <app_name> WITH YOUR APP NAME:

  • Add a new script for running migrations (./build_and_migrate.sh):
#!/usr/bin/env bash
# exit on error
set -o errexit

./build.sh

# Run migrations
_build/prod/rel/<app_name>/bin/<app_name> eval "Render.Release.migrate"
  • Make that new script executable.
  • Make a module in your application for running migrations (lib/<app_name>_web/render_release.ex):
defmodule Render.Release do
  @moduledoc """
    Responsible for custom release commands
  """
  @app :<app_name>

  def migrate do
    # Allows the migration script to connect to a database using SSL.
    Application.ensure_all_started(:ssl)

    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    # Allows the migration script to connect to a database using SSL.
    Application.ensure_all_started(:ssl)

    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  end

  defp repos do
    Application.load(@app)
    Application.fetch_env!(@app, :ecto_repos)
  end
end
  • Change the deploy command for your render app to be ./build_and_migrate.sh instead of ./build.sh

Using a Database that Requires SSL

Unless you're using a private database on Render, you'll want to connect to your PG instance using SSL. To do that, you've got to

  • Uncomment the line in releases.exs that says ssl: true for your app's repo:
...
config :<app_name>, <AppName>.Repo,
  ssl: true, <-- this line, it's commented by default.
  url: database_url,

And then

  • Add the :ssl application to extra_applications in mix.exs:
def application do
    [
      mod: {<AppName>.Application, []},
      extra_applications: [:logger, :runtime_tools, :ssl] <-- added :ssl to the end of this list.
    ]
  end

Troubleshooting

If you run into an issue with Phoenix Live View breaking and reloading the page every few seconds, you might have enabled force_ssl in your endpoint, and it could be trying to redirect your web socket traffic to an https scheme. Which is strange. I ran into this once, and haven't found a great workaround for it.

PgBouncer

If your database does pooling using pg_bouncer then open up releases.exs and add prepare: :unnamed to your repo config.

config :<app_name>, <AppName>.Repo,
  ssl: true,
  url: database_url,
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
  prepare: :unnamed <-- this line!

Authenticated Live View Tests

I've been running through Pragmatic Studio's Live View course, which I highly recommend, and wanted to make a specific note of how to authenticate users when writing Live View tests after adding phx_gen_auth to a Phoenix project.

Happily, this is a very short post, because phx_gen_auth does all the work for you. When you've run that generator, you get a function added to your conn_case.ex called register_and_log_in_user. This function takes a map with a connection in it, creates a new user, logs the user in, and returns it with a modified connection.

So now my test that used to fail because the page now requires authentication:

    test "saves new message", %{conn: conn} do
      {:ok, index_live, _html} = live(conn, Routes.message_index_path(conn, :index))

      assert index_live |> element("a", "New Message") |> render_click() =~
               "New Message

---

     ** (MatchError) no match of right hand side value: {:error, {:redirect, %{flash: "SFMyNTY.g2gDdAAAAAFtAAAABWVycm9ybQAAACRZb3UgbXVzdCBsb2cgaW4gdG8gYWNjZXNzIHRoaXMgcGFnZS5uBgCMD68zeQFiAAFRgA.zriUcbZsaCi0a4Bqq-hpJK0WP8IH-MHusGt3DPw028g", to: "/users/log_in"}}}

Will pass if I just call that function at the front:

    test "saves new message", %{conn: conn} do
      # 🚨 Add this line to authenticate the test request.
      %{conn: conn} = register_and_log_in_user(%{conn: conn})
      {:ok, index_live, _html} = live(conn, Routes.message_index_path(conn, :index))

      assert index_live |> element("a", "New Message") |> render_click() =~
               "New Message

Murphy’s setup guide for a new Phoenix (1.5) project

What follows is an extremely concise list of steps I'm keeping around for reference when setting up a new Elixir / Phoenix project with Tailwind, Alpine, and user auth.

NOTE: Pretending the name of the app is "love_notes"

Generate it

mix phx.new love\_notes --live

Use UUIDs

in config/config.exs

config :love_notes, :generators,
  migration: true,
  binary_id: true,
  sample_binary_id: "11111111-1111-1111-1111-111111111111"

Setup Tailwind

from https://pragmaticstudio.com/tutorials/adding-tailwind-css-to-phoenix

Install

cd assets
npm install tailwindcss @tailwindcss/forms postcss autoprefixer postcss-loader@4.2 --save-dev

Postcss

In assets/postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

Webpack

In your config:

use: [
  MiniCssExtractPlugin.loader,
  'css-loader',
  'postcss-loader',// <-- add this
  'sass-loader'
]

Tailwind Config

cd assets
npx tailwindcss init

Modify the config to add purge directories, set up the jit, enable dark mode, and add the tailwind UI plugin:

module.exports = {
  purge: [
    '../lib/**/*.ex',
    '../lib/**/*.leex',
    '../lib/**/*.eex',
    './js/**/*.js'
  ],
  mode: 'jit',
  darkMode: 'media',
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms')
  ],
}

Modify script in package.json

"deploy": "NODE_ENV=production webpack --mode production"

Include the CSS in the main file

in assets/css/app.scss

@tailwind base;
@tailwind components;
@tailwind utilities;

If you want to use component classes

You can do it like this.

@layer components {
  .btn-indigo {
    @apply bg-indigo-700 text-white font-bold py-2 px-4 rounded;
  }
}

Adding Alpine JS

npm i alpinejs

In app.js :

import 'alpinejs'

// other stuff

let liveSocket = new LiveSocket("/live", Socket, {
  dom: { // <- Add this 'dom' section
    onBeforeElUpdated(from, to){
      if(from.__x){ window.Alpine.clone(from.__x, to) }
    }
  },
  params: {_csrf_token: csrfToken}
})

Adding Auth

Inside of mix.exs

      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      # Add the following line 👇🏻
      {:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false}
    ]

Get deps and run the generator:

mix deps.get
mix phx.gen.auth Accounts User users
mix deps.get # Do this again so that the deps added by the generator get fetched.