DEV Community

Joe Nash for Heroku

Posted on

Deploying to Heroku from GitHub Actions

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.

A screenshot of the Actions tab within a new GitHub repository. Displayed is “Get started with GitHub Actions”, beneath which is the option to set up a simple starter workflow.

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

A screenshot of the Code tab after selecting to set up a simple workflow. A code editor holds a simple workflow, repeated in the page below this image. A side bar explains the workflow and presents the option to commit it to the repository.

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.

A screenshot of the Actions tab after the workflow has been triggered. The actions tab shows the steps of the workflow having completed successfully with a green tick.

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.

A screenshot of the Secrets menu within the Settings tab of a GitHub repository.

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.

A screenshot of creating a new secret. There are two inputs, a name field, and a value field. The name filled is filled with `HEROKU_API_KEY`, and the OAuth token is to fill the value field.

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:

The Actions tab showing a green tick against every step of the job to login to Heroku Container Registry.

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 Actions tab showing a green tick against every step of the job to build and release to Heroku Container Registry. The Release step is expanded to show the output of the successful command.

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.


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)

javaguirre profile image
Javier Aguirre

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:

    - uses: actions/checkout@v1
    - name: Push to Heroku
      run: git push -f https://heroku:${{ secrets.HEROKU_API_TOKEN }}${{ secrets.HEROKU_APP_STAGING }}.git origin/master:master
Enter fullscreen mode Exit fullscreen mode

What do you recommend?


joenash profile image
Joe Nash

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.

javaguirre profile image
Javier Aguirre

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!

lsgalves profile image
Leonardo Galves

Hi! great article, I was just curious about how the migration files would be executed. I tried this:

  - name: Migrate
    run: heroku container:run web ./ migrate -a ${{ secrets.HEROKU_APP_NAME }}
Enter fullscreen mode Exit fullscreen mode

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:

      HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
Enter fullscreen mode Exit fullscreen mode
sabiou profile image
Farouk Sabiou • Edited

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}
› Warning: heroku update available from 7.47.3 to 7.47.4.
▸ No images to push
Error: Process completed with exit code 1.

gusbemacbe profile image
Gustavo Costa

Change ubuntu-latest to ubuntu-20.04.

Add this source code before Login to Heroku Container registry

    - name: Update Heroku
        HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
      run: sudo apt update && sudo apt install heroku
Enter fullscreen mode Exit fullscreen mode
r0nunes profile image
Renato O. Nunes • Edited


I tried to add the code above, but I still have the same error

frenzyfunky profile image
Yağız Yorulmazlar

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.

shinjam profile image
Jae Min Shin • Edited

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!

damiensn profile image
Damien S

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 😅

manojap profile image


Run heroku container:push -a *** web
▸ Couldn't find that app.

[error]Process completed with exit code 1.

gusbemacbe profile image
Gustavo Costa

@manojap and @iamjithindas

Switch to a new source code:

name: Deploy

      - master

    runs-on: ubuntu-20.04
      - uses: actions/checkout@v2
      - name: Updating Ubuntu system and Heroku
          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
        run: sudo apt update && sudo apt install heroku neofetch
      - name: Checking the info
        run: neofetch

      - uses: akhileshns/heroku-deploy@v3.8.8
          heroku_email: your Heroku e-mail
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: your Heroku app names (go to your project's settings and check the app name)
          branch: HEAD
          useforce: false
          dynamicFiles: false
Enter fullscreen mode Exit fullscreen mode
iamjithindas profile image
Jithin das A

I ended up with the same error. have you found what's the problem?

landsprutte profile image

can you pass enviroment-vars to the container aswell? Say you have a connectionUrl in your secrets you want to pass :D

joenash profile image
Joe Nash

Yes you can! You just set them as inputs in the Action's metadata

guanacone profile image

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!

j_a_o_v_c_t_r profile image
João Victor Martins

Great article, man. Help me a lot. =)

ddgroleau profile image
Dan Groleau

Very helpful, thanks a lot!