Preparing a Phoenix 1.6 app for deployment with Elixir Releases

Preparing a Phoenix 1.6 app for deployment with Elixir Releases

Prepare a Phoenix 1.6 application for deployment with Elixir Releases

Updates

  • 20211009 added "Migrations support" section

I'm going to prepare a Phoenix application for deployment.

Prerequisites

Ensure to have your Phoenix 1.6 app running locally.

Runtime configuration

When deploying to production is better to inject runtime info to the application when it starts instead of having that info hardcoded in the source code. We pass info to the app to affect the way it works depending on the environment we are deploying the app to (e.g. staging, production).

Elixir 1.11 has introduced a way to inject this runtime info easily with the config/runtime.exs config file. If you open that file you'll see that it obtains some values from environment variables. The default environment variables to configure are POOL_SIZE, PORT, DATABASE_URL and SECRET_KEY_BASE. We need to specify a value for those envvars if we want our deployment to work correctly.

For now we are going to test it locally, in our laptop. In a deployment service, like Gigalixir or Fly.io, those envvars are going to be provided when the app starts. We are going to do that manually here:

export POOL_SIZE=2
export PORT=4001
export DATABASE_URL=ecto://postgres:postgres@localhost/saturn_dev
export SECRET_KEY_BASE=$(mix phx.gen.secret)

I have my database named locally saturn_dev and the user and password the ones shown. You can see your own connection parameters in config/dev.exs

Build in production mode

Compile elixir code

We can now get the production dependencies:

mix deps.get --only prod
MIX_ENV=prod mix compile

Compile assets

If the project has JS, CSS or other assets you can also compile them with the esbuild wrapper that phoenix now uses:

MIX_ENV=prod mix assets.deploy

Test that the project starts in prod mode

By now, if you haven't closed the terminal, you'll have the previous envvars still defined. If you have closed the terminal, you need to set them again.

MIX_ENV=prod mix phx.server

If you go to http://localhost:4001/ you'll see the homepage of the app, but this time it is using the configuration that the config/runtime.exs read from the terminal when it started instead of using the config/dev.exs configuration. One thing you'll notice is that the LiveDashboard link is gone. This works only in dev mode.

Phoenix App running in prod mode

Generate a release

We need to do an extra step before building the release using Elixir Releases. Open config/runtime.exs and uncomment the following line, in the section titled "Using releases"

config :saturn, SaturnWeb.Endpoint, server: true

This direct the app to start the webserver when running the release executable. When we used mix phx.server this was done for us. Now we need to explicitly enable it.

After saving those changes we can now generate the release:

MIX_ENV=prod mix release

Run the release

We can now run the release executable generated by the mix release task:

_build/prod/rel/saturn/bin/saturn start

If you go again to http://localhost:4001/ you'll see the app running, but this time from the self-contained bundle that the Elixir Releases generated for us.

Migrations support

There is one more thing before finishing. Right now we are using a database that was created by a mix ecto.create command. But the release we just generated has no support for running mix in production. There is no mix command anywhere inside the _build/prod/rel folder. So how are we going to create the database and to run the phoenix migrations? Good question. We need a workaround that is embedded in the application itself.

Create a file in lib/saturn/release.ex and put this content there:

defmodule Saturn.Release do
  @app :saturn

  def migrate do
    load_app()

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

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

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

  defp load_app do
    Application.load(@app)
  end
end

Save the file and regenerate the release:

MIX_ENV=prod mix release

Let's create a production database in our local postgres to simulate the production database in our production environment. Login to PostgreSQL locally create the database:

psql -U postgres -h localhost 
psql (13.4)
Type "help" for help.

postgres=# CREATE DATABASE saturn_prod;
CREATE DATABASE

You need to change the DATABASE_URL to point to the new database:

export DATABASE_URL=ecto://postgres:postgres@localhost/saturn_prod

And now you can run the migrations:

_build/prod/rel/saturn/bin/saturn eval "Saturn.Release.migrate"

and you should see something like this:

23:41:17.647 [info] Migrations already up

Now you can start the app and it will point to the saturn_prod database we just created:

_build/prod/rel/saturn/bin/saturn start

Go again to http://localhost:4001 and the app will work normally, but now the database is fully migrated.

That's it.

You can put the contents of the _build/prod/rel/saturn folder in your production server (as long as same architecture that the computer you used to assemble the release) and start it. You don't need anything else installed because this folder includes all the dependencies and binaries required to run the application. As long as you set the environment variables with correct values for production, this should work flawless.

You could do that manually, for example, by copying this folder to a DigitalOcean droplet or any other VPS provider, but there are better ways to do that.

I'll show you how to deploy to Gigalixir in a future post.

Cheers

About

I'm Miguel Cobá. I write about Elixir, Elm, Software Development, and eBook writing.

Photo by Gautier Salles on Unsplash