Now that we’ve leveled up our applications’ code, let’s level up our deployment workflow and infrastructure.
We’re still going to deploy to Heroku, but in a more robust way than we first learned how to.
This time, we’re going to take advantage of Heroku Pipelines to easily manage multiple deployment targets.
We’ll have an app for our customers just like before, which we’ll now call production
; but we’ll also have other apps: for beta testers, for demonstrating unreleased features, for quality assurance, for code review, etc
This will unlock powerful workflows wherein you can give and get continuous feedback on in-progress features every time you push a commit, even from non-technical stakeholders. Let’s get started!
If you want to follow along, you can set up a workspace with any application that’s ready to be deployed, even if it’s a brand new one. I’ll be using a repository that I created using the vanilla-rails template.
First, let’s dive deeper into Git “remotes”.
Whenever we’ve been pushing code, we’ve been doing something like:
git push origin main
or just git push
for short; which by default uses origin
for the third part, and whichever branch you have checked out for the fourth part.
The third part is the location that we want to send the code to, known as the “remote”. You can list out all of the remotes that you’ve got with the git remote
command:
gitpod /workspace/pipeline-demo:(main) $ git remote
origin
I happen to have one; you might have the same, or you might have more. origin
is just a nickname; we can see the actual URL of the location with git remote -v
:
gitpod /workspace/pipeline-demo:(main) $ git remote -v
origin https://github.com/raghubetina/pipeline-demo.git (fetch)
origin https://github.com/raghubetina/pipeline-demo.git (push)
This lets us know for sure where our code is going to and coming from when we push
and pull
. Typically, origin
is our primary repository on GitHub.com.
Importantly, we can add new remotes with the git remote add
command.
You don’t need to do this part, but for demonstration purposes, I am going to add a remote. Let’s say I want to make a redundant copy of the repository on Gitlab.com, a GitHub alternative, and push
to it from time to time for safekeeping.
First, I need to go to my Gitlab dashboard and create a repository. Then, they will assign the repository a Git URL, such as the following:
https://gitlab.com/raghubetina/pipeline-demo.git
Now I’m ready to add my remote and nickname it gitlab
:
gitpod /workspace/pipeline-demo:(main) $ git remote add gitlab https://gitlab.com/raghubetina/pipeline-demo.git
gitpod /workspace/pipeline-demo:(main) $ git remote -v
gitlab https://gitlab.com/raghubetina/pipeline-demo.git (fetch)
gitlab https://gitlab.com/raghubetina/pipeline-demo.git (push)
origin https://github.com/raghubetina-appdev/pipeline-demo.git (fetch)
origin https://github.com/raghubetina-appdev/pipeline-demo.git (push)
Now I’ve got two remotes. I can git push gitlab
whenever I choose, and my branch will be sent to Gitlab for safekeeping (provided I set up authentication).
The point is: we’re not limited to just one storage location for our repositories; we can add
as many remote
s as we like, and sending our code to them is as simple as git push
.
One last thing on Git remotes: you can see an overview of all of your remotes as well as some other Git configuration by opening the config file within the hidden .git
folder:
open .git/config
Take a look at it, but be careful with this file; you don’t want to make any errant keystrokes in here by mistake. I will sometimes edit the URLs of remotes directly in this file, but only very cautiously.
Now, back to Pipelines.
The thing that made Heroku revolutionary when they stormed the scene in 2009 was that they said “Okay, as long as you’re sending code around with git push
, send it to us that way too — and we’ll provision a server for you, put your code on it, spin up a database, set up a connection pool, and do the 1001 other things needed to get your app up and running. You just need to add us as an additional remote
and git push
a commit whenever it’s ready to ship.”
So, when we run the heroku create my-app-name
command, it actually does two things:
Through Heroku’s API, it creates an app and retrieves the assigned Git URL. If the app name we chose is available the Git URL would be https://git.heroku.com/my-app-name.git
.
We could do the same thing by going to our Heroku dashboard in the browser and creating a new app there.
It adds a Git remote called heroku
:
git remote add heroku https://git.heroku.com/my-app-name.git
We could do the same thing ourselves at the command line.
The presence of this remote is what enables us to deploy using:
git push heroku main
As we know, once the deployment completes, Heroku will assign the domain:
https://my-app-name.herokuapp.com
We will, for any real application, never allow our users to see that underlying .herokuapp.com
subdomain. We will purchase our own domain, e.g. www.my-app-name.com
, and configure it to point to our Heroku app; so that our users never know the difference.
From now on, within our remotes, we’re going to stop using the default nickname of heroku
. For our primary app, the one that our customers interact with, lets use the nickname production
.
As far as the app’s name within Heroku, my convention is end the name in -production
or -prod
— e.g., my-app-name-production
. Sometimes, the exact name I want will already be taken in Heroku; but that’s okay, because I’m going to mostly be referring to them by my local Git remote nicknames when I’m running commands. So just pick a name that’s close and available.
When initially creating an app using the heroku
command-line tool, you can choose a remote name using the -r
option.
gitpod /workspace/pipeline-demo:(main) $ heroku create pipeline-demo-production -r production
Creating ⬢ pipeline-demo-production... done
https://pipeline-demo-production.herokuapp.com/ | https://git.heroku.com/pipeline-demo-production.git
If you already have a remote named heroku
, you can rename it:
git remote rename heroku production
If you don’t already have a production
app, create one now and deploy your main
branch to it:
git push production main
Visit your application with heroku open
. Is it working? If so, yay!
If not, read the server log with heroku logs --tail
and debug. You probably need to heroku run rails db:migrate
, for one thing. Possibly also heroku run rails sample_data
if that makes sense for your application.
Woohoo! Even 10+ years later, it still brings a tear to my eye how much Heroku has simplified deployment 😢
Let’s say you want feedback on a feature that you’re working on from a client, a co-founder, or a designer who isn’t familiar with GitHub, GitPod, etc. It would be a huge amount of friction to ask them to sign up for accounts, rails db:create
, rails db:migrate
, rails sample_data
, bin/server
, etc.
Instead, let’s create a second Heroku app whose purpose will be for deploying experimental code to. We’ll call this one staging
:
heroku create pipeline-demo-staging -r staging
Let’s deploy to this one, too:
git push staging main
If we look at our list of remotes now, we should see origin
, production
, and staging
:
gitpod /workspace/pipeline-demo:(main) $ git remote
origin
production
staging
If you try to open your staging application with heroku open
, you should see an error:
gitpod /workspace/pipeline-demo:(main) $ heroku open
› Error: Multiple apps in git remotes
› Usage: --remote staging
› or: --app pipeline-demo-staging
› Your local git repository has more than 1 app referenced in git remotes.
› Because of this, we can't determine which app you want to run this command against.
› Specify the app you want with --app or --remote.
› Heroku remotes in repo:
› pipeline-demo-production (production)
› pipeline-demo-staging (staging)
›
› https://devcenter.heroku.com/articles/multiple-environments
The issue is: now that we have more than one Heroku app, we have to be more specific when we run our heroku
commands about which location we want them performed on.
heroku open -r staging
And if you have errors like before, to run the same commands on staging
, add the -r staging
flag:
heroku logs --tail -r staging
heroku run rails db:migrate -r staging
heroku run rails sample_data -r staging
Adding the -r production
or -r staging
flag to every heroku ...
command is a pain, but I’ll show you some shortcuts soon.
I now have two applications:
production
is for customers, and will be reachable at (for example) www.my-app-name.com
(once I add a custom domain).staging
is for testing or demonstration purposes (different teams use staging
differently), and will be reachable at (for example) staging.my-app-name.com
.Much better than only having a single deployment target! Now I can merge to main
, deploy to staging
, kick the tires in a real production environment, and then finally ship it to customers.
Now that we have our two apps up and running, let’s create a Heroku Pipeline to group them together.
Head over to your Heroku dashboard and, first, confirm that your two new apps appear in the list there. Create a new pipeline from the dropdown in the top-right:
Choose a name for your pipeline:
On the next screen, you will see Stages, Staging and Production. Add your apps to their respective pipeline. (If you hadn’t already created your apps from the command-line with heroku create
, you could have created them from here, added them as remotes with git remote add
, and then done your initial deploys with git push
).
One immediate benefit of grouping the apps together in a Pipeline is that, once you’re fully confident in a change, you can promote your staging app’s slug directly to the production app. This results in a faster deployment, in some cases with less downtime, than git push production main
.
Imagine there are 10 developers on the team with 2-3 branches each that they think are ready to merge into main
and deploy to production
. They are just waiting for approval from Quality Assurance and the product owners.
If you only have a single staging
server, you’re going to have a big traffic jam. Should each developer create their own staging server? Since we’re using Heroku, with it’s unbelievably good deployment ergonomics, that is actually within the realm of possibility; if we were working directly with e.g. Amazon AWS for hosting, it would be out of the question. However, there’s a better way: Review Apps.
To enable Review Apps in our Pipeline, click the “Connect to GitHub” button, authorize Heroku to access your GitHub account (don’t forget to Grant access to any organizations you want) and locate the repository that your origin
remote is pointing at. Finally, click the “Enable” button next to “Enable Review Apps”:
In the pane that opens, check off “Create new review apps for new pull requests automatically” and “Destroy stale review apps automatically”:
If your application requires any environment variables, click the “Reveal Config Vars” button and add them.
Now, try the following:
Create a new feature branch. E.g.,
git checkout -b a-new-feature
pipeline-demo
, I made a change to public/index.html
and committed it.Voilá! Heroku automatically detected the new pull request, immediately provisioned a new app, and deployed the feature branch (not main
) to it.
main
.git pull
ing your branch, switching to it, possibly running database migrations, etc, in order to interact with your feature while providing feedback.In my experience, Review Apps dramatically tighten feedback loops between product owners, developers, clients, designers, usability testers, and stakeholders all throughout the development cycle. This is one of the most important Continuous Delivery techniques that we’ll add to our arsenal.
There’s just one problem: when the Review App is done building and you visit it, it’s quite likely that you probably see the familiar “Something went wrong” error, due to the familiar “pending migrations” issue. Ugh.
We don’t have a remote for the Review App, so we can’t do the usual thing of heroku run rails db:migrate
with the -r
flag.
Instead, we’ll use the -a
flag with the app name. You can find the assigned Heroku app name in the pipeline, or in the pull request on GitHub. By default, it will be the pipeline name followed by a random string:
heroku run rails db:migrate -a pipeline-demo-pho-z9a9qp
You can also, in your Review App settings, configure to have predictable names based on PR numbers: pipeline-demo-pr-1.herokuapp.com
. If you don’t mind Review App URLs being guessable, you might prefer these slightly more convenient URLs; particularly when you need to use the -a
flag with the heroku
CLI.
Let’s continue to make our deployment workflow even smoother.
Here’s something that used to happen to me every almost every single time I deployed, and probably just happened to you a minute ago:
git push main production
heroku open
heroku logs --tail
heroku run rails db:migrate -r production
Argh! I always forget to rails db:migrate
. Can’t we just tell Heroku to always rails db:migrate
whenever we deploy, just in case there are any new migrations?
Why yes, we can! Heroku allows you to include a file called Procfile
in the root folder of your application, in which you can specify commands that you want to be executed upon startup.
There’s a lot of things that you can include in a Procfile
, but here’s a good starting point that you can use for your applications:
# /Procfile
web: bundle exec puma -p $PORT -C ./config/puma.rb
release: bundle exec rails db:migrate
web:
, is how you tell Heroku what commands to run when each Web dyno starts. Heroku does a pretty good job with Rails apps by default, but here you can fine tune it if you want. In the example above, we’re telling Heroku to launch the Puma web server on the default port using the configuration we specified in our config/puma.rb
file.release:
, is how we tell Heroku any commands we want to run every time we deploy a new version of the app. Here’s our chance to automatically rails db:migrate
— phew!Happily, the Procfile
runs for Review Apps just like any other apps, which takes care of the issue we ran into above. If you create a Procfile
with the above contents in the root folder of your application, commit, and push to your branch, the Review App should re-deploy and the database ought to be automatically migrated. Yay!
In addition to a Procfile
, Heroku allows us to include an app.json
file in the root of our application to describe other details about how to deploy it. Here, we can say things like what add-ons (like Scheduler or Redis) to include, what environment variables we require, how many web and worker dynos to spin up, etc.
The app.json
file is ignored during the regular git push
deployment process, but it is respected during Review App deployment (and other Platform API deployments, like if you want to include a “Deploy to Heroku” button in your README).
For Review Apps, there’s one thing particularly important about app.json
: the ability to specify a command to run only after the initial deploy, as opposed to after every release (as in the Procfile
). This allows us to run e.g. rails sample_data
automatically, which is a huge benefit for Review Apps (but we wouldn’t want to do it for e.g. production
).
Here is a minimal example app.json
:
{
"name": "Minimal Heroku",
"scripts": {
"postdeploy": "bundle exec rails db:seed sample_data"
},
"formation": {
"web": {
"quantity": 1
}
}
}
A real one would likely include add-ons, a worker dyno, environment variables, etc. Read more about app.json
at the official docs.
If we add an app.json
now to the app that we’ve been experimenting with, commit, and push, it won’t take effect until you destroy the Review App in your pipeline and then re-build it again (or close the Pull Request in GitHub and re-open it), since the postdeploy
script only runs once upon the initial deployment.
But then, you should see that the database migrated automatically (from the Procfile
) and the sample data (if we had a sample_data
task) is ready to go for reviewers to play around with (from app.json
). Yay!
Even though we’ve done some pretty sweet automation of some of the most frequently run heroku
commands (rails db:migrate
is now handled by Procfile
, rails db:seed
and rails sample_data
are handled by app.json
), we still use the heroku
command a lot. A brief selection of the top-level commands that we run a million times a day:
heroku logs
heroku console
heroku domains
heroku certs
heroku addons
heroku pg
Now that we have production
and staging
remotes (at least), we’re going to have to tack -r production
and -r staging
to the end of all these commands. That’s gonna get old real fast.
Fortunately, our friends at thoughtbot felt the same way and wrote a handy library to make it less painful: Parity.
Once installed, you now have two new commands available: production
and staging
. These are a lot like heroku
, but imagine they automatically have the -r production
or -r staging
tacked on to the end. In other words,
heroku domains:add -r staging
becomes just:
staging domains:add
heroku logs --tail -r production
becomes just:
production tail
Check out the Parity README for more.
This deployment technique combined with our Git branch→pull request→code review→merge workflow is very powerful. It works especially well with an iterative product management process for continuously delivering useful software and improving quickly based on feedback.
Even when I end up deploying on Fly.io, AWS, Render.com, Digital Ocean, or anywhere else, I strive to implement a Heroku Review App-like workflow, because I find it so productive. I wanted you to see it so that you have a solid baseline to compare against!