Skip to main content
This lesson shows how to implement the Continuous Delivery (CD) stage of your GitHub Actions pipeline: deploying tested builds to production (Heroku or a self-hosted Ubuntu/VPS server). The recommended pattern is:
  • Keep build and test steps in a dedicated build job.
  • Create a separate deploy job that runs only if the build job succeeds.
  • Use vetted Marketplace Actions for common targets (Heroku, SSH) to simplify authentication and deployment logic.
Below are clear YAML examples, important notes, and screenshots to help you set up a reliable GitHub Actions CD workflow.

Jobs: Build then Deploy

Organize your pipeline by separating CI (build/test) from CD (deploy). By default, GitHub Actions runs jobs in parallel. Make the deploy job depend on the build job using needs: [build] so it runs only after a successful build. Example workflow skeleton with two jobs:
Then the deploy job depends on the build job:
Split build/test and deploy into separate jobs and use needs: [build] for the deploy job. This ensures deployment only runs after the build and test jobs complete successfully.

Deploying to Heroku using a Marketplace Action

For Heroku, prefer a maintained Marketplace Action to avoid scripting credential setup and git pushes manually. A popular option is akhileshns/heroku-deploy. Here is where you might search for Heroku-related Actions in the Marketplace (example screenshot):
A web browser screenshot of the GitHub Marketplace showing search results for "Heroku" under Actions, with a list of Heroku-related GitHub Actions and category filters on the left. The page displays multiple action cards (Heroku Awake, Heroku Run, Deploy to Heroku, etc.) and a search bar at the top.
Create repository secrets for your Heroku credentials from GitHub > Settings > Secrets (or environment-specific secrets). Example adding the HEROKU_API_KEY secret:
A GitHub repository settings page with a pop-up dialog to add a secret named "HEROKU_API_KEY" (value filled in) and an "Add secret" button. The background shows the settings sidebar (Options, Security & analysis, Environments, Secrets, etc.).
Find your Heroku API key on your Heroku Account Settings and your app name in the Heroku dashboard:
A screenshot of the Heroku dashboard showing the Settings page for an app named "fastapi-sanjeev," displaying app information, config vars, buildpacks, and SSL certificate sections. Browser tabs and the desktop taskbar are visible around the page.
Example deploy job using the Marketplace action:
This action takes care of logging in, creating the Heroku remote, and pushing your code—so you can keep your workflow concise and secure.

Manual Heroku deploy (custom workflow)

If you need to control every step, you can use a manual .netrc approach that writes Heroku credentials to ~/.netrc and then pushes to the Heroku git remote:
Note: Using git remote add with .netrc lets you authenticate non-interactively. If you prefer heroku git:remote, add a step to install the Heroku CLI first.

Confirming Deploy Runs Only on Successful Builds

Because the deploy job uses needs: [build], any failure in the build job (tests or dependency installation) will skip the deploy job automatically. This prevents deploying broken code. When a test fails, GitHub Actions shows the build failure and the deploy job will be pending/skipped. Example failed workflow view:
A screenshot of a GitHub repository Actions page showing a failed workflow ("broke stuff Build and Deploy Code #23") where the build job failed (exit code 1) and the deploy step did not run. The workflow diagram shows "build" with a red X and "deploy" pending.
Example pytest failure that stops the build:
When this happens, the deploy job remains skipped—exactly what you want.

Deploying to an Ubuntu Server via SSH

To deploy to a self-managed server (Ubuntu VPS), use a Marketplace SSH action that runs remote commands. A commonly used option is appleboy/ssh-action. You can search SSH-related actions in the Marketplace (example screenshot):
A screenshot of the GitHub Marketplace showing search results for "ssh-action" with a list of SSH-related GitHub Actions (e.g., Slim SSH Action, Setup SSH Action). The left sidebar shows filter options like Types and various Categories.
Basic usage pattern:
  • Store server connection info as secrets: PROD_HOST, PROD_USERNAME, and either PROD_PASSWORD or a private key secret.
  • Use the SSH action to run commands that pull the latest code and restart services.
Example using appleboy/ssh-action with password-based auth and piping the password into sudo -S for systemctl restart:
If sudo on the remote server requires a password, use sudo -S and pipe the password from a secret (echo ${{ secrets.PROD_PASSWORD }} | sudo -S <command>) or configure passwordless sudo for the deploy user in /etc/sudoers. Passwordless sudo for specific commands is generally safer than embedding full privileged passwords in CI scripts.
If you initially run into this error on the remote server:
Adjust the command to use sudo -S or set up an appropriate sudoers entry and re-run the pipeline. After fixing this, the Actions run status should show green for both build and deploy:
Screenshot of a GitHub repository's Actions page showing a list of workflow runs with green check and red X status icons, workflow names, and timestamps. The left sidebar shows "All workflows" and a filter/search box.

Quick Reference: Marketplace Actions Comparison


Best Practices & Summary

  • Use separate build and deploy jobs and connect them with needs: [build] to enforce deployment only on successful builds.
  • Prefer Marketplace Actions (for Heroku and SSH) to reduce scripting and common credential pitfalls.
  • Store all sensitive values in GitHub Secrets or environment-level secrets: HEROKU_API_KEY, HEROKU_APP_NAME, HEROKU_EMAIL, PROD_HOST, PROD_USERNAME, PROD_PASSWORD, etc.
  • For Heroku use akhileshns/heroku-deploy to automate authentication and pushes.
  • For SSH deployments use appleboy/ssh-action and handle sudo either by piping the password with sudo -S or configuring passwordless sudo for deploy tasks.
  • The CI will stop on failures (tests/install errors) and the deploy job will be skipped due to the needs dependency — preventing broken code from reaching production.
This completes the Continuous Delivery setup for deploying tested builds to Heroku or your Ubuntu production server via GitHub Actions.

Watch Video