guide, deploy, elixir 1.9, phoenix, render.com, migrations

Deploy Phoenix App to Render.com

Deploying Elixir Phoenix applications is a broad subject but in this guide we're going to limit the process to deploying a simple greenfield app to render.com.

Render.com is described as:

Render is a unified platform to build and run all your apps and websites with free SSL, a global CDN, private networks and auto deploys from Git.

Overview

  • Create a simple Elixir Phoenix application.
  • Configure and deploy our new Phoenix app to Render.com
  • Deployment using releases available in Elixir 1.9+

Notes on Releases

You can read the release notes for Elixir 1.9 here, but let's cover some of the important points.

Releases allow developers to precompile and package all of their code and the runtime into a single unit.

Elixir and Erlang VM are packaged within a release and don't need to be installed on the target machine.  Releases have very much simplified how Elixir applications are deployed and managed in production.

If you're interested in reading about the finer details, there are also some detailed docs on Releases in Hex.

Create an App

IMPORTANT: ensure that you are running Elixir 1.9+ before continuing:

$ elixir -v

With this in mind, let's create our throwaway app:

$ mix phx.new render
...
Fetch and install dependencies? [Yn]

Then setup the local version:

$ cd render
$ mix ecto.create

We can start the server, but we already know what's going to happen.  Let's move on.

Configure Releases

The first step is to rename config/prod.secret.exs.  We will be using environment variables to configure the app, so we won't need any secrets.

$ mv config/prod.secret.exs config/releases.exs

This will be the runtime configuration for the release.  Open releases.exs and make a few changes:

import Config # Change 1: be sure to replace use Mix.Config

database_url =
  System.get_env("DATABASE_URL") ||
    raise """
    environment variable DATABASE_URL is missing.
    For example: ecto://USER:[email protected]/DATABASE
    """

config :render, Render.Repo,
  # ssl: true,
  url: database_url,
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

secret_key_base =
  System.get_env("SECRET_KEY_BASE") ||
    raise """
    environment variable SECRET_KEY_BASE is missing.
    You can generate one by calling: mix phx.gen.secret
    """

config :render, RenderWeb.Endpoint,
  http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
  secret_key_base: secret_key_base,
  server: true  # Change 2: add server: true

This is the final version that includes the changes we need to make, and clears out the comments.  Feel free to keep the comments if they'll help you understand what's going on in the config.

Next, open config/prod.exs and remove the reference to prod.secret.exs:

import_config "prod.secret.exs

Build Script

We're going to use a build script to give render a series of commands to run in order to deploy our app.  Create a new file bin/render-build with the following content:

#!/usr/bin/env bash
# Initial setup
mix deps.get --only prod
MIX_ENV=prod mix compile

# Compile assets
npm install --prefix ./assets
npm run deploy --prefix ./assets
mix phx.digest

# Remove the existing release directory and build the release
rm -rf "_build"
MIX_ENV=prod mix release

# Run migrations
_build/prod/rel/render/bin/render eval "Render.Release.migrate"

And set the permissions on the file:

$ chmod a+x bin/render-build

Release Commands

In the build script, you may have noticed we have the line that runs a migrate function in the Render.Release module. Here's the purpose:

Another common need in production systems is to execute custom commands required to set up the production environment. One of such commands is precisely migrating the database. Since we don't have Mix, a build tool, inside releases, which are a production artifact, we need to bring said commands directly into the release.

To add the custom module, create LIB_PATH/release.ex with this content:

defmodule Render.Release do
  @moduledoc """
    Responsible for custom release commands
  """
  @app :render

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

  def rollback(repo, version) do
    {: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

This gives the both the migrate and rollback commands from inside the release.

App Configuration

Next we need to update our production config for deployment to render.com.

Open config/prod.exs and update the app's url:

config :render, RenderWeb.Endpoint,
  url: [host: "example.com", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

To this:

config :render, RenderWeb.Endpoint,
  url: [host: System.get_env("RENDER_EXTERNAL_HOSTNAME") || "localhost", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

RENDER_EXTERNAL_HOSTNAME is set by render automatically.

This should get us most of the way there.  Let sign up on render.com and create our services.

Push to Github

To make simplify deploying to render.com, create a repo in your Github account, and push the code:

$ git init
$ git remote add origin <ssh-endpoint>
$ git add .
$ git commit -m 'Init'
$ git push origin

Render.com Setup

With our github repo ready, you can register on render.com using your Github account.  

Create a DB Service:

Dashes aren't accepted, use underscores 

Create Web Service:

And while that's creating, create the web service under Services -> New Web Service and select the repo created earlier.  If you didn't connect your Github account, you can still do it now.

  • Name: `<something-unique>`
  • Environment: Elixir
  • Branch: master
  • Build Command:  ./bin/render-build
  • Start Command: _build/prod/rel/render/bin/render start

Hit create the service and it will fire off an initial attempt to deploy the app.

You'll likely see an error in the log:

Which means our app can't connect the database.  This is expected because we haven't configured it yet.

Environment Variables

We're going to configure a few environment variables necessary to run the app.

Navigate to the "Databases" section of the render.com dashboard, and select the DB you just created.

Then copy the "Internal Connection String":

render.com database dashboard

Then navigate to the services section and select your newly created service.

Hit the "Environment" tab:

Where we can enter  DATABASE_URL and the "Internal Connection String"

Next we can generate a new secret key using:

$ mix phx.gen.secret

And set SECRET_KEY_BASE to the output value.

Test it out

If you monitor the deployment log, there shouldn't be any issues at this point and we have our first release in production.

You can test it out by visited the service url in the browser.

Auto Deploy

In the settings section for your app you can toggle auto-deploys on commits. Any changes made to the environment variables will also trigger a deployment.

Conclusion

We really enjoy working with render.com and though there are many other options for deployment, we're finding that $14/mth is a small price to pay for a solid, production ready deployment pipeline.

There are many other features that we didn't get to in the guide such as PR deploys/review apps, background services and the infrastructure as code using yaml files, but maybe we'll dive deeper into these features if there's enough interest.

I hope you enjoyed this guide and let us know if you have any questions for feedback on how to improve it.

Follow us on twitter for updates: @phxroad

Author image

About Troy Martin

Husband, Father & Software Developer who tinkers with writing and building things.
  • Hamilton, ON