[010.3] Continuous Deployment to Heroku

Deploying to Heroku, then setting up Semaphore to continuously deploy on each push to master.

Subscribe now

Continuous Deployment to Heroku [08.02.2017]

In the last episode, we set up continuous integration for Firestorm via Semaphore. Today we'll deploy our app to Heroku, then set up Semaphore to deploy each time the tests pass on the master branch. Let's get started.

Project

We're starting with the dailydrip/firestorm repo tagged before this episode.

I've already signed up for Heroku and installed the Heroku Toolbelt, which is Heroku's CLI.

Heroku created the concept of buildpacks, which are a way to tell Heroku how to set up and run your application. I'll be using the Heroku Elixir Buildpack.

I'll create an app named firestorm-forum, specifying the buildpack:

heroku create firestorm-forum --buildpack "https://github.com/HashNuke/heroku-buildpack-elixir.git"

We need to add another buildpack, to handle the static compilation for our frontend assets.

heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-static.git

We need to tweak our configuration a little to fetch some environment variables from Heroku:

vim config/prod.exs
# We'll enable SSL and force its use, and fetch the SECRET_KEY_BASE environment
# variable.
config :firestorm_web, FirestormWeb.Web.Endpoint,
  # ...
  url: [scheme: "https", host: "firestorm-forum.herokuapp.com", port: 443],
  force_ssl: [rewrite_on: [:x_forwarded_proto]],
  secret_key_base: System.get_env("SECRET_KEY_BASE")
# ...
# We'll use the POOL_SIZE var and let Heroku tell us how to talk to our database
config :firestorm_web, FirestormWeb.Repo,
  adapter: Ecto.Adapters.Postgres,
  url: System.get_env("DATABASE_URL"),
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
  ssl: true
# and we don't need a prod secrets file so we can remove that

We're not using Phoenix Channels yet, but we will in the future. We need to reduce the timeout time for the websocket transport so that connections are closed before Heroku's 55 second timeout:

vim lib/firestorm_web/web/channels/user_socket.ex
defmodule FirestormWeb.Web.UserSocket do
  # ...
  transport :websocket, Phoenix.Transports.WebSocket,
    timeout: 45_000
  # ...
end

Finally, we'll create a Procfile. This is what Heroku uses to determine how to start your application:

vim Procfile
web: MIX_ENV=prod mix phx.server

Now, let's create our database on Heroku:

heroku addons:create heroku-postgresql:hobby-dev

And we'll configure its pool size:

heroku config:set POOL_SIZE=18

We get 20 connections to our database on the hobby tier, so we'll configure it to be slightly below that number so we have a couple of spare connections to use for things like migrations and mix tasks.

When we do run a mix task, we can limit the number of connections it attempts to create to make sure we don't exceed the headroom:

# example:
# heroku run "POOL_SIZE=2 mix ecto.migrate"

Let's create the secret key and set our Heroku environment variable:

mix phoenix.gen.secret
# copy its output
heroku config:set SECRET_KEY_BASE="the_previous_output"

We have a few more environment variables that we need to make available to our app as well. We can configure this by creating an elixir_buildpack.config file:

vim elixir_buildpack.config
config_vars_to_export=(AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_S3_BUCKET AWS_S3_REGION GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET)

Some of these env vars are secret, so I'll set them off-screen. Now, we're ready to deploy. Let's add the files we changed and push it up to our repo:

git checkout -b feature/episode_010.3
git add config/prod.exs lib/firestorm_web/web/channels/user_socket.ex elixir_buildpack.config
git commit -m"Add Heroku configuration"
git push origin feature/episode_010.3

Now we can deploy to Heroku. Normally you would just push master up to Heroku, but we'll push this feature branch to Heroku's master branch instead:

git push heroku feature/episode_010.3:master

On a Phoenix application that uses Brunch, this would be sufficient. We're using webpack though, so there's a little more work that has to go into it. To get Heroku to build our assets with webpack, we can create a compile script that the buildpack will use:

vim compile
#!/bin/bash

NODE_ENV=production ./node_modules/.bin/webpack -p
cd ..
mix phx.digest
chmod +x compile
git add .
git commit -m"Add custom compile script to handle webpack"
git push origin feature/episode_010.3
git push heroku feature/episode_010.3:master

If we push this, it's still not quite enough - there's an error when uglify tries to run. Let's add a .babelrc for our assets:

vim assets/.babelrc
{
  "presets": [
    ["es2015", { "modules": false }]
  ]
}
git add .
git commit -m"Add babelrc for assets"
git push origin feature/episode_010.3
git push heroku feature/episode_010.3:master

With this, everything pushes up and builds successfully. If we try to visit the site, it won't work though. We can check the logs to find out why:

heroku logs

We haven't run our migrations. Let's do that:

heroku run "POOL_SIZE=2 mix ecto.migrate"
heroku ps:restart

Now if we visit the app, it works. Mostly. We still haven't setup the frontend to hit the deployed URL, and we'd really like to set up continuous deployment so it gets deployed each time we push.

First, I'll configure the frontend to hit this API:

vim assets/config/production.js
module.exports = {
  apiBaseUrl: "https://firestorm-forum.herokuapp.com/api/v1/"
};

Before we push this, we'll set up semaphore for continuous deployment.

First, we'll click Set up deployment on semaphore for the project:

Semaphore set up deployment

We'll click 'Heroku':

Semaphore deploy with heroku

We'll pick automatic:

Semaphore deploy automatically

Then we'll pick the branch we want to deploy from.

Next, I have to paste my Heroku API key, which I'll do off-screen. Then we pick the app to deploy and complete setup.

We can also modify the deployment steps to add a migration step after each deploy:

heroku run --exit-code -- MIX_ENV=prod POOL_SIZE=2 mix ecto.migrate

Now we can push the js config change up, just to our github this time - we'll let semaphore handle the Heroku deployment.

git add .
git commit -m"Set up js production url"
git push origin feature/episode_010.3

Once the tests pass, it will deploy to Heroku for us. And that's it!

Summary

Today we deployed Firestorm to Heroku and set up Continuous Deployment via Semaphore. My preference with Elixir is to do a deployment to a bare machine, rather than using a PaaS such as Heroku. However, I think it's important to show this off, and honestly it's a very fast way to get Firestorm up and running. I hope you enjoyed it. See you soon!

Resources