How to Set Up Jenkins for Continuous Deployment to Heroku

How to Set Up Jenkins for Continuous Deployment to Heroku

Heroku’s own Git-based deploy workflow is convenient, but the moment you need tests, approvals, or multi-environment promotion, plain git push heroku main stops being enough. I ran into this early on a side project that grew real users, and moving deploys into Jenkins gave me the control I was missing without giving up Heroku’s simplicity. This guide covers the full setup, from a blank Jenkins install to a pipeline that tests, builds, and ships to Heroku automatically.

Why Put Jenkins in Front of Heroku

Heroku already does continuous deployment from GitHub out of the box, so the value Jenkins adds is everything around the deploy itself: running your full test suite, gating on code coverage, running security or lint checks, deploying to a review app before promoting to production, and giving you a single dashboard across Heroku and any other infrastructure you run.

Jenkins Architecture Overview

Heroku deployments from Jenkins typically happen one of two ways: pushing to Heroku’s Git remote, or using the Heroku CLI/API to deploy a pre-built slug or container image. Both are covered below.

Prerequisites

Step 1: Install the Heroku CLI on the Jenkins Agent

curl https://cli-assets.heroku.com/install.sh | sh
heroku --version

Step 2: Generate a Heroku API Key

heroku login
heroku authorizations:create

Copy the generated token. Store it in Jenkins under Manage Jenkins > Credentials as a “Secret text” credential with an ID like heroku-api-key.

Step 3: Install Jenkins Plugins

There’s no dedicated “Heroku plugin” required for most setups — the Heroku CLI handles everything, so Jenkins just needs to run shell commands with the right credentials injected.

Step 4: Method A — Git Push Deployment

This approach pushes your build directly to Heroku’s Git remote, letting Heroku’s buildpacks handle the build.

pipeline {
    agent any

    environment {
        HEROKU_APP_NAME = 'my-heroku-app'
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/yourorg/myapp.git'
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'npm test'
            }
        }

        stage('Deploy to Heroku') {
            steps {
                withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
                    sh '''
                        git remote remove heroku || true
                        git remote add heroku https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git
                        git push heroku HEAD:main -f
                    '''
                }
            }
        }
    }

    post {
        success {
            echo "Deployed to Heroku app: ${HEROKU_APP_NAME}"
        }
    }
}

Step 5: Method B — Container Registry Deployment

If your app is Dockerized, deploying via Heroku’s container registry gives you more control over the build environment and is often faster for complex build steps.

stage('Build and Push Container') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                echo $HEROKU_API_KEY | docker login --username=_ --password-stdin registry.heroku.com
                docker build -t registry.heroku.com/$HEROKU_APP_NAME/web .
                docker push registry.heroku.com/$HEROKU_APP_NAME/web
            '''
        }
    }
}

stage('Release Container') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                export HEROKU_API_KEY=$HEROKU_API_KEY
                heroku container:release web --app $HEROKU_APP_NAME
            '''
        }
    }
}

Multi-Environment Promotion with Heroku Pipelines

Heroku has its own concept of “pipelines” (staging → production promotion). You can combine this with Jenkins by deploying to a staging app first, running smoke tests, and then using the Heroku CLI to promote:

stage('Deploy to Staging') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                git push https://heroku:$HEROKU_API_KEY@git.heroku.com/${HEROKU_APP_NAME}-staging.git HEAD:main -f
            '''
        }
    }
}

stage('Smoke Test Staging') {
    steps {
        sh 'curl -f https://${HEROKU_APP_NAME}-staging.herokuapp.com/health'
    }
}

stage('Promote to Production') {
    steps {
        input message: 'Promote staging build to production?'
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                export HEROKU_API_KEY=$HEROKU_API_KEY
                heroku pipelines:promote -a ${HEROKU_APP_NAME}-staging
            '''
        }
    }
}

This mirrors a blue-green style promotion, ensuring nothing hits production without passing a real health check first.

Managing Config Vars and Secrets

Never hardcode secrets in the Jenkinsfile or repository. Set Heroku config vars via the CLI as part of setup, not on every deploy:

heroku config:set DATABASE_URL=$DB_URL --app my-heroku-app

If config needs to change per-deploy (rare), inject via Jenkins Credentials and set it as a pipeline step guarded by withCredentials.

Real-World Workflow

  1. Developer opens a PR → Jenkins runs lint and unit tests only, no deploy.
  2. PR merges to main → Jenkins builds, tests, and deploys to a Heroku staging app automatically.
  3. A smoke test hits the staging health endpoint; failure halts the pipeline.
  4. A manual approval step gates promotion to the production Heroku app using heroku pipelines:promote.
  5. Post-deploy, Jenkins tails Heroku logs briefly to confirm no immediate crash loop.
stage('Post-Deploy Log Check') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                export HEROKU_API_KEY=$HEROKU_API_KEY
                timeout 20 heroku logs --app $HEROKU_APP_NAME --tail || true
            '''
        }
    }
}

Security Best Practices

Troubleshooting

Handling Database Migrations Alongside Deploys

Heroku apps backed by Postgres often need a migration step run as part of the deploy, not before or after it disconnected from the release. Use a release phase entry in your Procfile so Heroku runs migrations automatically as part of every dyno release, or trigger it explicitly from Jenkins right after a successful deploy:

stage('Run Database Migrations') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                export HEROKU_API_KEY=$HEROKU_API_KEY
                heroku run "npm run migrate" --app $HEROKU_APP_NAME
            '''
        }
    }
}

Running migrations through heroku run executes them in a one-off dyno against the production database, which is safer than baking migration logic into your app’s boot sequence, since a failed migration won’t take down already-running web dynos.

Scaling Dynos as Part of the Pipeline

For predictable traffic patterns (say, scaling up ahead of a marketing push, or down overnight to save cost), Jenkins can manage dyno scaling directly:

stage('Scale Dynos') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                export HEROKU_API_KEY=$HEROKU_API_KEY
                heroku ps:scale web=3:standard-2x --app $HEROKU_APP_NAME
            '''
        }
    }
}

This can be wired to a scheduled Jenkins job (using triggers { cron(...) }) independent of the deployment pipeline, giving you time-based autoscaling without needing a paid Heroku add-on.

Monitoring Deploys with Heroku’s Metrics API

After a deploy completes, it’s worth having Jenkins pull a quick snapshot of dyno health rather than assuming a successful push means a healthy application:

stage('Check Dyno Health') {
    steps {
        withCredentials([string(credentialsId: 'heroku-api-key', variable: 'HEROKU_API_KEY')]) {
            sh '''
                export HEROKU_API_KEY=$HEROKU_API_KEY
                heroku ps --app $HEROKU_APP_NAME | grep -q "up" || \
                  (echo "No dynos reported as up after deploy" && exit 1)
            '''
        }
    }
}

This is a cheap but effective safety net — if the release process succeeded but the app crash-looped on boot, this stage catches it immediately instead of leaving it for a user to discover.

FAQs

Can Jenkins trigger a deploy automatically on every push to main? Yes, using a GitHub webhook configured to trigger the Jenkins job on push events to the main branch.

Do I still need Heroku Review Apps if I’m using Jenkins? They’re complementary — Review Apps are great for PR-level preview environments, while Jenkins handles testing, gating, and controlled promotion to staging/production.

Can this pipeline deploy multiple Heroku apps from one repo (monorepo)? Yes, parametrize HEROKU_APP_NAME and repo subdirectory per stage, or use separate Jenkinsfiles per service.

What happens if the Heroku deploy fails halfway through? Heroku’s release mechanism is atomic per dyno restart — a failed build won’t take down the currently running version, but you should still add a rollback stage using heroku rollback for release-level issues.

Summary

Jenkins and Heroku aren’t competitors — Jenkins handles the testing, gating, and promotion logic, while Heroku still does what it’s best at: painless application hosting. Whether you deploy via Git push or container registry, the pattern is the same: test first, deploy to staging, verify, then promote to production behind a manual or automated gate.

References

Exit mobile version