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.

Elixir and the Wall of Tests

Here's an experience I tend to have over and over:

  • Generate a new Elixir / Phoenix project
  • Use the super cool Phoenix generators to make a new model
  • Change the model substantially
  • Realize that the tests generated with the model are now super broken

When I get into this situation running mix text produces a huge number of errors. For some reason, I always want to scroll to the top of the error list and work on the first one produced, which makes fixing all of the errors a painful process of save, clear terminal, run command, scroll up, repeat.

Run just one test 🧘

The first thing I do to get some presence of mind and move forward in this situation is stop running the tests. No, not all the tests, though it certainly can be tempting to delete the file and forget about testing. No, just tell the test runner to one a single test at a time.

In ex-unit, Elixir's testing framework which comes built-in to Phoenix, this can be done with what they call "tags".

What are tags?

Test tags are a very flexible feature that allows the programmer to categorize tests throughout the project, and include or exclude whole selections of test based on their tags.

For example?

One example of that comes to mind would be tagging "slow" tests and configuring them only to run before a merge into a repository's main branch.

Back to the topic

Okay, back on topic, here's how we use tags to run just one test at a time. First, find the first test in a broken suite and add this line above its declaration:

@tag :focus
test "does a thing when I do a thing", %{
...

Here I've declared that this test is tagged with the atom "focus". Now, I can run just this test by adding the --only tag to the test command:

mix test --only focus

Now, all of the other tests will be skipped, and only the tagged test will be run. When I've fixed that test, I'll move the tag to the next test in-line. If I've fixed all the tests in a describe block, and I want to verify that they're all working together, I can move that @tag :focus line right above the describe block, and all of the test inside of it will be run. Hurrah!

Keyboard fatigue 🥱

Another thing that gets tiresome when working through the wall of tests is command-tabbing to my terminal, up-arrowing, and enter-ing after every file change. mix_test_watch to the rescue! This package will re-run your tests for you after every file change, and it accepts all the same arguments as the normal test command!

Just add it to your mix.exs file:

      ...
      {:phoenix_ecto, "~> 4.1"},      
      # 👇 Add this line
      {:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
      ...

And and change test to test.watch in your CLI command:

mix test.watch --only focus

That's a wrap 🌯

If you're like me, these two steps will help you actually see what's going wrong with the broken tests, instead of just closing your terminal in fear of that massive wall of red text and browsing Twitter until you forget why you were procrastinating in the first place.