Deploy Elixir Phoenix to Heroku Containers with Docker

We’re going to deploy Elixir and Phoenix to the Heroku Containers Stack with Elixir releases, automated database migrations, Docker, and Continuous Deployment. Heroku was rated Best for Simple Setups and Easy Integrations in our article covering the Best Cloud Hosting Providers for Elixir and Phoenix. Heroku has a completely free tier for Hobby projects and is a good option for simple Elixir apps.

Overview

Here are the topics we’ll cover while deploying the project.

  1. Create Elixir Phoenix Project
  2. Prepare App to Deploy on Heroku
  3. Deploy Elixir App on Heroku with Continuous Deployment

Prerequisites

To follow this guide, make sure you have at least the following applications installed on your machine. In the brackets are the versions with which this guide was written.

  • Elixir (1.12.3-otp-24)
  • Erlang (OTP 24.1)
  • Phoenix (1.5.13)
  • Node.js with npm (15.14.0)

Make sure to install at least Elixir 1.9 since we are using releases. We recommend using asdf to manage Erlang and Elixir versions.

If you run into problems, the finished project is available on GitHub as StakNine HelloHeroku.

Create Elixir Phoenix Project

We are going to create a simple Phoenix project called HelloHeroku. Use the command line to update your Phoenix version and create a new Phoenix project.

Create new Phoenix app

mix archive.install hex phx_new 1.5.13
mix phx.new hello_heroku

Change into the project directory, create your database and start your app.

cd hello_heroku
mix ecto.create
mix phx.server

Now visit http://localhost:4000 and you should see the Welcome to Phoenix! page.

Add users

Now we want to add users to the project with the phx.gen html task. This allows us to verify the Postgres database is deployed properly for our project.

mix phx.gen.html Accounts User users name:string \
username:string:unique

Follow the directions to add the resource to the router.ex file and migrate the database with mix ecto.migrate.

Now start your server with mix phx.server and visit http://localhost:4000/users on your local machine to see the index of users. Add a new user to test out the database.

Add user to Elixir Phoenix app

We’ll use this project to test our database is migrated and our app is deployed properly.

Let’s commit our changes and then configure for releases.

git init
git add .
git commit -m 'initialize repo and add users'

Next we’ll update the project to use releases.

Update for Elixir releases

Render uses Elixir mix releases in its deployment process. A release consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine and runtime. Deploying with mix releases instead of Mix unlocks many of Elixir’s features.

We are going to update our project to build and start releases on your local machine.

Runtime configuration

Update the project to use runtime configuration with the steps below. With runtime configuration, you are able to store environment variables in an external configuration system in an actual production setup. We are going to use config/runtime.exs added in Elixir 1.11.

  1. Change config/prod.exs to no longer call import_config "prod.secret.exs" at the bottom.
  2. Rename config/prod.secret.exs environment file to config/runtime.exs.
  3. Change use Mix.Config to import Config inside the new config/runtime.exs file.
  4. Restrict your runtime configuration to the :prod environment
  5. Uncomment or add server: true.
# config/runtime.exs
import Config

if config_env() == :prod do
  # other configuration from prod.secret.exs

  config :hello_render, HelloHeokuWeb.Endpoint, server: true

end

Ecto migrations and custom commands

You can’t run mix ecto.migrate using releases because Mix is not available. We need to write a release commands file. Create a new file in your application, lib/hello_render/release.ex, with the following:

defmodule HelloHeroku.Release do
  @app :hello_heroku

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

  def rollback(repo, version) do
    ensure_started()

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

  defp ensure_started do
    Application.ensure_all_started(:ssl)
  end

  defp repos do
    Application.load(@app)
    Application.fetch_env!(@app, :ecto_repos)
  end
end

One change from the Phoenix documentation is to add Application.ensure_all_started(:ssl) to each function using ensure_started/0. Since Render uses SSL to connect to the database, you need to start SSL before running your migrations. Read more on ElixirForum.

Deploy Your Elixir Release Locally

Once you’ve completed those sections, you should be able to test your app and start building releases.

First set the following environment variables.

# generate a really long secret
mix phx.gen.secret

export SECRET_KEY_BASE=REALLY_LONG_SECRET
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/hello_heroku_dev

Then load dependencies to compile code and assets.

# Initial setup
mix deps.get --only prod
MIX_ENV=prod mix compile

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

Next drop the existing database and create a new database, so you can test the release command. Then run the mix command to build the release with the MIX_ENV=prod mix release build script.

mix ecto.drop
mix ecto.create
MIX_ENV=prod mix release

Migrate your database and start your application

_build/prod/rel/hello_heroku/bin/hello_heroku eval "HelloHeroku.Release.migrate"
_build/prod/rel/hello_heroku/bin/hello_heroku start

Now you should be able to visit http://localhost:4000/users on your local machine to see the index of users. You can add a user again to test out the site is working properly.

Add user Elixir Releases

Great! Let’s commit our changes and then prepare for deployment.

git add .
git commit -m "update for releases"

Now we’re ready to update our Phoenix application to deploy our project to Heroku.

Prepare App to Deploy on Heroku

There are two different ways to deploy a Phoenix app on Heroku. We could use Heroku buildpacks or the Heroku container stack. The difference between these two approaches is in how we tell Heroku to build our app.

In the Heroku buildpack case, we need to update our apps configuration on Heroku to use Phoenix/Elixir specific buildpacks.

With the container approach, we have more control on how we want to set up our app and we can define our container image using Dockerfile and heroku.yml.

We’ve already set up our app to use Elixir releases. Now we will update our app to deploy to the Heroku container stack with Docker.

Update Heroku config

First, update config/prod.exs with the following changes to the HelloHerokuWeb.Endpoint config. Don’t forget to replace still-river-71145 with your application name.

config :hello_heroku, HelloHerokuWeb.Endpoint,
  http: [port: {:system, "PORT"}],
  url: [scheme: "https", host: "still-river-71145.herokuapp.com", port: 443],
  force_ssl: [rewrite_on: [:x_forwarded_proto]],
  cache_static_manifest: "priv/static/cache_manifest.json"

Then update config/runtime.exs to use ssl for your database connection in production. You should be able to simply uncomment ssl: true. Note: we added Application.ensure_all_started(:ssl) to lib/hello_heroku/release.ex to make sure SSL is started before our database migrations.

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

Finally, if you plan on using websockets, then we will need to decrease the timeout for the websocket transport in lib/hello_heroku/endpoint.ex. If you do not plan on using websockets, then leaving it set to false is fine.

defmodule HelloHeroku.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello_heroku

  socket "/socket", HelloHeroku.UserSocket,
    websocket: [timeout: 45_000],
    longpoll: false

  ...
end

This ensures that any idle connections are closed by Phoenix before they reach Heroku’s 55-second timeout window.

This completes Heroku config updates, next we’ll add our entrypoint.sh, Dockerfile and heroku.yml files.

Add entrypoint.sh

We’ll use an entrypoint script to run our migrations before we start our application.

#!/bin/bash
# docker entrypoint script.

# wait until Postgres is ready
while ! pg_isready -q -d $DATABASE_URL
do
  echo "$(date) - waiting for database to start"
  sleep 2
done

bin="/app/bin/hello_heroku"

# migrate the database
echo "starting Migrations"
eval "$bin eval \"HelloHeroku.Release.migrate\""

# start the elixir application
echo "starting Application"
exec "$bin" "start"

Add Dockerfile

Here is a sample Dockerfile to add to the root folder of your application. We started with the Phoenix guide’s sample Dockerfile with the following updates:

  1. Updated the alpine versions
  2. Install python3 instead of python
  3. Added bash and postgresql-client for entrypoint.sh
  4. Use entrypoint.sh instead of start command to execute migrations.
FROM elixir:1.11.4-alpine as build

# install build dependencies
RUN apk add --no-cache build-base npm git python3

# prepare build dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force && \
    mix local.rebar --force

# set build ENV
ENV MIX_ENV=prod

# install mix dependencies
COPY mix.exs mix.lock ./
COPY config config
RUN mix do deps.get, deps.compile

# build assets
COPY assets/package.json assets/package-lock.json ./assets/
RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error

COPY priv priv
COPY assets assets
RUN npm run --prefix ./assets deploy
RUN mix phx.digest

# compile and build release
COPY lib lib
# uncomment COPY if rel/ exists
# COPY rel rel
RUN mix do compile, release

# prepare release image
FROM alpine:3.14.2 AS app
# added bash and postgresql-client for entrypoint.sh
RUN apk add --no-cache openssl ncurses-libs bash postgresql-client

RUN mkdir /app
WORKDIR /app

COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/hello_heroku ./
COPY entrypoint.sh .

RUN chown -R nobody: /app
USER nobody

ENV HOME=/app

CMD ["bash", "/app/entrypoint.sh"]

Add heroku.yml

Add a new heroku.yml file to your root folder. In this file you can define addons used by your app, how to build the image and what configs are passed to the image.

setup:
  addons:
    - plan: heroku-postgresql
      as: DATABASE
build:
  docker:
    web: Dockerfile
  config:
    MIX_ENV: prod
    SECRET_KEY_BASE: $SECRET_KEY_BASE
    DATABASE_URL: $DATABASE_URL

Let’s commit all our changes.

git add .
git commit -m "Add heroku config and files"

Next up we’ll create our app on Heroku and deploy!

Deploy Elixir App on Heroku with Continuous Deployment

We’ll use the Heroku CLI to create our application, set environment variables, and deploy our app. Then we’ll use the Heroku dashboard to set up Continuous Deployment with Heroku’s GitHub integration.

Signing up for Heroku

First create a Heroku account. The Free plan will give you one web dyno and one worker dyno, as well as a PostgreSQL and Redis instance for free.

Installing the Heroku Toolbelt

Once you have signed up, you can use brew to install the Heroku Toolbelt.

brew tap heroku/brew && brew install heroku

If you’re not using macOS, download Heroku Toolbelt for other operating systems here.

The Heroku CLI, part of the Toolbelt, is useful to create Heroku applications, list currently running dynos for an existing application, tail logs or run one-off commands (mix tasks for instance).

Create Heroku application

First create the app using the Heroku CLI. Then set the stack of your app to container, this allows us to use Dockerfile to define our app setup.

heroku create
Creating app... done, ⬢ still-river-71145

heroku stack:set container

Create Database and Environment Variables in Heroku

The DATABASE_URL config var is automatically created by Heroku when we add the Heroku Postgres add-on. We can create the database via the Heroku CLI:

heroku addons:create heroku-postgresql:hobby-dev

Now we set the POOL_SIZE config var:

heroku config:set POOL_SIZE=18

This value should be just under the number of available connections, leaving a couple open for migrations and mix tasks. The hobby-dev database allows 20 connections, so we set this number to 18. If additional dynos will share the database, reduce the POOL_SIZE to give each dyno an equal share.

We still have to create the SECRET_KEY_BASE config based on a random string. First, use mix phx.gen.secret to get a new secret:

mix phx.gen.secret
a-really-long-string

Now set it in Heroku:

heroku config:set SECRET_KEY_BASE="a-really-long-string"

Depoy with Heroku CLI

Our project is now ready to be deployed on Heroku.

git push heroku main
# output...
remote: Verifying deploy... done.
To https://git.heroku.com/still-river-71145.git
 * [new branch]      main -> main

Check out your app at https://my-app-111.herokuapp.com and test you can add a user like we did on our local machine.

Now that your app is deployed, you can see the health and performance metrics using the Heroku dashboard.

Heroku Dashboard

Now that you’ve deployed using the Heroku CLI, we’ll set up Continuous Deployment from GitHub.

What is Continuous Integration / Continuous Deployment (CI/CD)?

According to Red Hat, the "CI" in CI/CD always refers to continuous integration, which is an automation process for developers. Successful CI means new code changes to an app are regularly built, tested, and merged to a shared repository.

The "CD" in CI/CD refers to continuous delivery and/or continuous deployment.

Continuous delivery usually means a developer’s changes to an application are automatically bug tested and uploaded to a repository (like GitHub), where they can then be deployed to a live production environment by the operations team.

Continuous deployment (the other possible "CD") can refer to automatically releasing a developer’s changes from the repository to production.

In our example, we only plan to set up Continuous Deployment. However, you can add a GitHub Actions workflow file to set up Continuous Integration using GitHub Actions.

Continuous Deployment from GitHub

First, you need to push your project to a [GitHub](https://github.com] repo.

Then to set up Continuous Deployment, click on the Deploy tab, connect your GitHub account, and connect to your GitHub project repo.

Connect Heroku and GitHub for Continuous Deployment

That’s it! Going forward, every push to your GitHub repo will automatically build your app and deploy it in production. If the build fails, Heroku will automatically stop the deploy process and the existing version of your app will keep running until the next successful deploy.

Conclusion

Congratulations! You went through the steps to deploy your Phoenix Elixir app to the Heroku containers stack using Elixir releases, automated database migrations, and Docker. You also set up Continuous Deployment for your project to automatically deploy to Heroku when you push commits to your default (e.g. main) branch on GitHub. Now that you’ve deployed to production, check out our article covering the best error monitoring services for Elixir so you can deploy with confidence and effectively manage errors in your system.

Want To Know How To Deploy Phoenix Apps Using A single Command?

This brand-new FREE training reveals the most powerful new way to reduce your deployment time and skyrocket your productivity… and truly see your programming career explode off the charts!