GitHub Actions give us a new way to deploy to Heroku, and to integrate Heroku with other parts of our development workflows. In a single GitHub Actions workflow, we could lint our Dockerfile and package configs, build and test the package on a variety of environments, compile release notes, and publish our app to Heroku.
Today we are going to build a simple workflow that will use the Heroku CLI to deploy a project with a Dockerfile to Heroku via the Heroku Container Registry. During the course of building this workflow, we will see how to create jobs, configure the runner environment with GitHub secret store, use public actions, and react to and filter events.
What is GitHub Actions?
GitHub Actions are a way to trigger custom workflows in response to events on GitHub. For example, a push could trigger Continuous Integration (CI), a new issue being opened could trigger a response from a bot, or a pull request being merged could trigger a deployment.
Workflows, jobs, and actions
Workflows are made up of jobs. These jobs are performed by a runner in a virtual environment. Jobs are composed of individual steps, and those steps can be scripts executed on the runner, or an action. Actions are composable units of workflows, that can be built in Docker containers or JavaScript, placed directly in your repository, or included via the GitHub Marketplace or Docker registry.
Getting started
To get started, push a project with a Dockerfile to GitHub, or fork an existing repository. Don’t forget that Heroku provides an environment variable, $PORT
, for apps to bind to for HTTP traffic, so be sure to adjust the project to listen to that variable, rather than an explicitly set port.
Within the repository, navigate to the Actions tab.
On first opening the Actions tab, we’ll be presented with some options for getting started. These include CI setups for popular languages, as well as a simple example workflow. Let’s start by selecting that simple workflow. Rename the file to workflow.yml
, and hit “Start commit”.
Your first workflow
After the workflow is setup, the repository will have a .github/workflows
folder, with a workflow.yml
file inside.
This workflow defines one job, build
, with 3 steps. One of those steps uses
an existing, public action, checkout
. The others are defined within the job, with a name
, and a run
field. run
executes commands on the runner performing the job. This job will use an Ubuntu machine. All GitHub Actions environments have the same specs, but you can run jobs on Ubuntu, MacOS, and Windows.
checkout
is an important action that you will use in most workflows that work on the code in the repository. checkout
fetches the contents of your repository to $GITHUB_WORKSPACE
, an environment variable that maps to /home/runner/work
on the runner. checkout
, like many first-party actions provided by GitHub, is open source and viewable on the Actions GitHub Organization.
This workflow runs on the push
event, that is any time new contents are pushed to the repository. You can see the workflow and the status of its run under the Actions tab.
Inside the workflow, you can see that each of the named steps has logs that can be expanded. Naming and describing the steps in your workflows can help this interface provide rich information on the state of your deployments.
Whilst the repository contents are fetched to the runner with checkout
, the workflow does not yet make any use of them. Let’s start working towards having this Dockerised project published to a Heroku app.
Environment variables and GitHub Secret store
You can deploy to the Heroku Container Registry with either the Heroku CLI, or Docker. Fortunately, both are available within the virtual environment provided to the runners, when using Ubuntu. Using the Heroku CLI will give us access to other useful utilities, so let’s start there.
Browserless Heroku CLI Authorization
In order to build or deploy, you will have to login to the Container Registry. When using the Heroku CLI locally, login is via the browser, but the Heroku CLI can also be authenticated by providing an OAuth token.
To create an OAuth authorisation, on your local machine, run:
heroku authorizations:create
.
This will create a long-lived user authorization, whose token can be used to authenticate the Heroku CLI in our workflow.
The Heroku CLI expects this token to be found in an environment variable, HEROKU_API_KEY
. You can define environment variables within job steps, but you don’t want to insert the key directly and commit it to the repository. Luckily, GitHub Actions comes with a new Secrets store, within the repository settings.
There is a prompt to add a new secret.
Within Secrets, create a new secret, HEROKU_API_KEY
, and insert the token given by the Heroku CLI.
Going back to our workflow.yml
, add a new step named “Login to Heroku Container Registry”. Within this step you need to define the environment variable HEROKU_API_KEY
by grabbing the secret, followed by running container:login
with the Heroku CLI.
Workflow environment variables
Environment variables are defined with env
. The workflow can access the contents of the repository secret store through a Context. There are many Contexts available which hold information about the workflow run, the job being performed, the runner environment, and the secret store. Retrieve the secret from the secrets
Context and assign it to an environment variable like so:
With that environment variable available, you can now run heroku container:login
to log into the Heroku Container Registry with the OAuth token.
Commit that code and push it to the repository to trigger the workflow.
Back under the Actions tab, your step will execute and log you in successfully:
Build and Release to Heroku Container Registry
Now that the Heroku CLI is authenticated against the Heroku Container Registry, you can push your project to be built, and then release the resulting container.
Create two new steps, one for the push, and the other for release. Each will also need a declaration of the HEROKU_API_KEY
environment variable, as environment variables are not persisted between steps.
When pushing and releasing the container, you can specify an app name to target. This could be included safely in the workflow .yml, but as the app name is used in multiple commands, and you may want to change it later, let’s add it to the secret store and access it via the secrets
context.
Commit and push those changes, and return to the GitHub repository Actions tab to check on the build:
The build has completed successfully, and you should now be able to visit your deployed app on Heroku.
Events and Filters
Right now, the push
event is triggering this workflow, regardless of branch, or file, that new code is pushed to. You will likely want to tailor this trigger depending on your development practice. For example, if using the GitHub Flow, you may want to deploy to a staging environment when a pull request is merged to master.
Workflow syntax gives us the ability to filter on branches, and files, as well as to trigger on all of the available GitHub webhook events. Let’s modify our push
event to filter only for pushes to master
.
Commit that, and push to master. Now any subsequent commits to branches other than master
will not trigger this workflow.
Summary
You’ve now created a GitHub Actions workflow, that:
- On a push to
master
- Fetches the contents of the repository
- Logs into Heroku Container Registry
- Builds the container on Heroku
- Publishes the project to an Heroku app
What’s next?
From this workflow, you can add new steps, and jobs, for other tasks in your development process. For example, why not use the Docker Lint action to lint the Dockerfile, before pushing to Heroku Container Registry?
There are also other ways to implement Heroku in a GitHub Actions workflow. This example used the Heroku Container Registry, via the Heroku CLI already installed on the runner virtual environment. The virtual environments also come with Git, so with minimal modification, you could use this workflow to deploy projects without a Dockerfile.
Top comments (18)
Hi! thank you for the article!
We also push from GH Actions to Heroku, but we just use git push since it seems it's easier, what's the difference doing it this way or just pushing? Does the build takes less time or more? Thanks!
What we do is:
What do you recommend?
Thanks!
Hey Javier!
That’s awesome to hear. It’s not a straight pro/con toss-up, and which builds faster is going to depend on your stack, and that for the most part is also going to decide whether git or docker is better for you.
Docker is going to give you greater flexibility in what you run on Heroku, as you are providing the container. But git is going to be using the Heroku-curated environments that they do a lot of work to optimise around. There’s also, I believe, some features that are less supported for Docker, for example I believe Review Apps isn’t available if you go a certain route for docker deploys.
Hi! We're using docker, heroku.yml, and the container stack! But instead of pushing to the container registry we use git push to Heroku, I don't understand if there is any difference in doing it your way or the way we do since both are docker deploys.
Thank you for your answer!
Hi! great article, I was just curious about how the migration files would be executed. I tried this:
But I get "the input device is not a TTY".
And you can add an environment variable for all the steps added to your job, like this:
Hi, thanks for the article. I followed the same step to configure an heroku deployment but I get this error:
Run heroku container:push -a habu-server web
heroku container:push -a habu-server web
shell: /bin/bash -e {0}
env:
HEROKU_API_KEY: ***
› Warning: heroku update available from 7.47.3 to 7.47.4.
▸ No images to push
Error: Process completed with exit code 1.
Change
ubuntu-latest
toubuntu-20.04
.Add this source code before
Login to Heroku Container registry
Hey!!
I tried to add the code above, but I still have the same error
Is your Dockerfile in a subdirectory? If so, change the Dockerfile file name as "Dockerfile.web" and modify heroku push command as "heroku container:push -a $APP_NAME --context-path . --recursive"
In here recursive searches Dockerfiles in current and subdirectories, context-path is specifies build context which is root directory.
Thank you very much for the article ! I found it very helpful.
But as I'm not really an expert in Docker I was wondering how you can pass environement variables for the build of the container and for the execution ?
I don't know if what I say is really clear, let me know if it isn't 😅
Hi, thank you for the article :)
I wonder how it is possible to use Heroku CLI even there aren't any codes about Heroku installation.
Does ubuntu-latest install by default or about actions/checkouts?
Thank you!
ENDED UP WITH THIS ERROR
Run heroku container:push -a *** web
▸ Couldn't find that app.
[error]Process completed with exit code 1.
@manojap and @iamjithindas
Switch to a new source code:
I ended up with the same error. have you found what's the problem?
can you pass enviroment-vars to the container aswell? Say you have a connectionUrl in your secrets you want to pass :D
Yes you can! You just set them as inputs in the Action's metadata
Great article and everything was working perfectly till I added environmental variables in my project and now the build is failing. Could you elaborate a bit more on how to pass the environmental variables to the container? Thanks a lot!
Great article, man. Help me a lot. =)
Very helpful, thanks a lot!