Skip to main content

πŸŸͺ Deploying to Heroku Platform

After the sprawling control panel of AWS, Heroku feels like a breath of fresh air: you git push, and a running app appears. This lesson unpacks how that magic works β€” dynos, buildpacks, config vars, and add-ons β€” and how to take a Heroku app from prototype to production-ready.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain Heroku's core building blocks β€” dynos, buildpacks, slugs, config vars, add-ons
  • Deploy a Node.js and a Python app via git push, GitHub integration, or container registry
  • Bind the app to the PORT environment variable and write a correct Procfile
  • Attach managed databases and Redis as add-ons and read them from config vars
  • Use pipelines and review apps for a staging β†’ production workflow
  • Recognize Heroku's limits and when to reach for an alternative

Estimated Time: 40–50 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Deploy a full-stack React + Express app to Heroku end to end.

In This Lesson

Why Heroku?

Heroku is a Platform as a Service (PaaS). You hand it your code; it handles the servers, the operating system, scaling, load balancing, and routing. There is no infrastructure to provision and no server to SSH into.

πŸ’‘ The concierge analogy: AWS is like renting an apartment and furnishing it yourself β€” total control, total responsibility. Heroku is a serviced hotel: you bring your suitcase (your code), and the staff handle housekeeping, utilities, and the front desk. You trade some control for enormous convenience.
flowchart LR A[Developer] --> B[git push heroku main] B --> C[Build: buildpack detects language] C --> D[Compile into a slug] D --> E[Run on dynos] E --> F[Live app] G[Add-ons] --> E H[Config vars] --> E

βœ… Where Heroku shines

  • Speed to first deploy β€” a working app in minutes, not hours
  • Developer experience β€” deploy with the Git you already know
  • Managed add-ons β€” Postgres, Redis, monitoring bolt on with one command
  • Built-in workflow β€” pipelines, review apps, and rollbacks included

⚠️ Where it's the wrong tool

  • Cost at scale β€” convenience gets pricey once you run many dynos
  • Limited control β€” you can't tune the underlying infrastructure
  • Fewer regions than the hyperscalers, so global latency can suffer
  • No free tier β€” Heroku removed free dynos in November 2022; the cheapest paid tier is the Eco plan (~$5/month for a pool of dyno hours)

How Heroku Works

A handful of concepts explain almost everything Heroku does. Learn these names and the CLI commands stop feeling like magic incantations.

mindmap root((Heroku app)) Dynos Web dynos Worker dynos One-off dynos Build Buildpacks Slug Config Config vars Releases Add-ons Postgres Redis Monitoring

πŸ“– The core vocabulary

Dyno: a lightweight, isolated Linux container that runs one process of your app. Web dynos handle HTTP requests; worker dynos process background jobs; one-off dynos run a task and exit (like a database migration).

Buildpack: a script that detects your language and installs dependencies. Official buildpacks cover Node.js, Python, Ruby, Java, PHP, Go, and more.

Slug: the compressed, ready-to-run package the buildpack produces from your code. Dynos boot from the slug.

Config var: an environment variable (e.g. DATABASE_URL) that customizes behavior without changing code.

Add-on: a managed third-party service (Postgres, Redis, logging) attached to your app.

Release: an immutable snapshot of slug + config + add-ons. Every deploy creates one, and you can roll back to any previous release.

⚠️ The filesystem is ephemeral. Anything a dyno writes to its local disk vanishes when the dyno restarts (which happens at least daily). Never store uploads or state on the dyno β€” put files in S3 and state in Postgres or Redis. This is the single most common Heroku beginner trap.

Deploying Your App

Two rules every Heroku app must follow

1. Listen on process.env.PORT. Heroku assigns your dyno a port at runtime and routes traffic to it. Hard-coding port 3000 will fail.

const express = require('express');
const app = express();

// Heroku injects PORT; fall back to 3000 for local dev
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello from Heroku!');
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

2. Declare your processes in a Procfile at the project root β€” a plain text file naming each process type and its command:

# Procfile
web: node server.js
worker: node worker.js

For Python you'd run through a production WSGI server:

# Procfile (Python)
web: gunicorn app:app

Method 1 β€” Git deployment (the classic path)

# Create the app (adds a 'heroku' git remote)
heroku create my-awesome-app

# Ship it
git push heroku main

# Open it in the browser
heroku open

Method 2 β€” GitHub integration (auto-deploy)

In the Heroku Dashboard, open your app's Deploy tab, choose GitHub, connect your repository, and enable Automatic Deploys from main. Every merge now deploys itself β€” no local push required.

Method 3 β€” Container registry (bring your own Docker)

If you'd rather ship the exact image you built and tested, push a Docker container instead of source:

heroku container:login
heroku container:push web -a my-app
heroku container:release web -a my-app

Node.js walkthrough

A minimal Node app needs a package.json with a start script and a pinned Node version:

{
  "name": "nodejs-example",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.19.2"
  },
  "engines": {
    "node": "20.x"
  }
}
git init
git add .
git commit -m "Initial commit"

heroku create nodejs-example-app
git push heroku main
heroku ps:scale web=1     # ensure one web dyno is running
heroku open

Python (Flask) walkthrough

Flask needs requirements.txt, a Procfile, and a production server (gunicorn):

# app.py
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def home():
    return 'Hello from Flask on Heroku!'

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port)
# requirements.txt
flask==3.0.3
gunicorn==22.0.0
heroku create flask-example-app
git push heroku main
heroku open

Add-ons: Databases & Config

Because the dyno filesystem is ephemeral, real apps store data in add-ons β€” managed services Heroku provisions and connects for you.

Attach a Postgres database and a Redis cache. (The old hobby-dev tier is retired; the current entry tiers are essential-0 for Postgres and mini for Redis.)

# Managed PostgreSQL β€” sets DATABASE_URL automatically
heroku addons:create heroku-postgresql:essential-0

# Managed Redis β€” sets REDIS_URL automatically
heroku addons:create heroku-redis:mini

# List and inspect
heroku addons
heroku config:get DATABASE_URL

Attaching Postgres sets the DATABASE_URL config var for you. Your code reads it from the environment β€” never hard-code credentials:

// db.js β€” connect using the injected DATABASE_URL
const { Sequelize } = require('sequelize');

const databaseUrl = process.env.DATABASE_URL || 'postgres://localhost:5432/local_db';

const sequelize = new Sequelize(databaseUrl, {
  dialect: 'postgres',
  dialectOptions: {
    ssl: { require: true, rejectUnauthorized: false }, // Heroku Postgres requires SSL
  },
});

async function testConnection() {
  try {
    await sequelize.authenticate();
    console.log('Database connection established.');
  } catch (error) {
    console.error('Unable to connect:', error);
  }
}

testConnection();
module.exports = sequelize;

Manage your own config vars (secrets, feature flags) directly:

heroku config:set NODE_ENV=production JWT_SECRET=super-secret-value
heroku config                    # list all config vars
heroku config:unset OLD_FLAG     # remove one

Run one-off tasks β€” like database migrations β€” on a temporary dyno:

heroku run npx sequelize-cli db:migrate
heroku run npx sequelize-cli db:seed:all
πŸ’‘ This is the Twelve-Factor way. Storing config in the environment (not in code) is one of the Twelve-Factor App principles Heroku's own founders wrote. The same discipline makes your app portable to any other platform later.

Pipelines & Review Apps

A pipeline chains several apps into a continuous-delivery workflow: code flows through review β†’ staging β†’ production, and you promote a tested build forward instead of rebuilding it.

graph LR A[GitHub repo] --> B[Review app per PR] B --> C[Staging app] C --> D[Production app] E[Automated tests] --> B E --> C
# Create a pipeline with a staging stage
heroku pipelines:create my-pipeline --stage staging -a my-staging-app

# Add the production app
heroku pipelines:add my-pipeline -a my-production-app --stage production

# Promote the exact staging slug to production (no rebuild)
heroku pipelines:promote -a my-staging-app

Review apps spin up a disposable, fully running copy of your app for every pull request, so reviewers click through the actual change before merging. Configure them with an app.json at the repo root:

{
  "name": "My Application",
  "description": "Full-stack demo app",
  "env": {
    "NODE_ENV": { "value": "review" }
  },
  "addons": ["heroku-postgresql:essential-0"],
  "buildpacks": [{ "url": "heroku/nodejs" }],
  "scripts": {
    "postdeploy": "npx sequelize-cli db:migrate"
  }
}

Everyday operations you'll reach for constantly:

# Stream live logs
heroku logs --tail

# Scale out (more dynos) or up (bigger dynos)
heroku ps:scale web=3 worker=2
heroku ps:resize web=standard-2x

# Roll back to the previous release if a deploy goes bad
heroku releases
heroku rollback

βœ… Production-readiness checklist

  • App is stateless β€” no reliance on local disk (uploads go to S3)
  • Secrets live in config vars, not in the repo
  • HTTPS enforced; run heroku certs:auto:enable for a custom domain
  • Background work runs on worker dynos, not blocking web requests
  • Database has automated backups (heroku pg:backups:schedule)
  • Logging + monitoring add-on attached; alerts configured

Limits & Alternatives

Heroku's simplicity comes with real constraints. Know them before you build so nothing surprises you in production.

  • Ephemeral filesystem β€” disk writes are lost on restart
  • 30-second request timeout β€” long operations must move to worker dynos or async jobs
  • Router body limits β€” large uploads should go straight to object storage, not through a dyno
  • Cost at scale β€” many dynos and premium add-ons add up quickly
  • Regional coverage β€” fewer regions than AWS/GCP/Azure

If you outgrow Heroku, several platforms offer a similar developer experience:

PlatformStrengthBest for
RenderHeroku-like DX, generous free static hostingModern web apps, startups
RailwaySlick UI, GitHub-native, persistent volumesSide projects, small teams
Fly.ioGlobal edge deploy, persistent volumes, WebSocketsLatency-sensitive, global apps
DigitalOcean App PlatformPredictable pricing, DO ecosystemTeams already on DigitalOcean
AWS Elastic BeanstalkPaaS feel with AWS depth underneathGrowing into the AWS ecosystem

πŸ’‘ Migrating out is easier if you planned for it

Because Heroku pushes you toward stateless, config-in-environment, Twelve-Factor design, a well-built Heroku app is already close to container-ready. Wrap it in a Dockerfile, move state to managed services, and most platforms will run it with minimal changes.

Hands-on Exercise

πŸ‹οΈ Deploy a Full-Stack App to Heroku

Goal: deploy a React frontend + Express backend as a single Heroku app, where the API serves the built React files in production.

Prerequisites

  • Node.js and Git installed
  • The Heroku CLI installed and a Heroku account (heroku login)

Step 1 β€” Server that serves the React build

// server.js
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 5000;

app.use(express.json());

app.get('/api/info', (req, res) => {
  res.json({ message: 'Hello from the backend!', env: process.env.NODE_ENV });
});

// In production, serve the compiled React app
if (process.env.NODE_ENV === 'production') {
  app.use(express.static(path.join(__dirname, 'client', 'build')));
  app.get('*', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
  });
}

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Step 2 β€” Build the client automatically on deploy

Add a heroku-postbuild script so Heroku compiles the React app during its build phase:

{
  "scripts": {
    "start": "node server.js",
    "heroku-postbuild": "cd client && npm install && npm run build"
  },
  "engines": { "node": "20.x" }
}

Step 3 β€” Deploy

git init && git add . && git commit -m "Initial commit"

heroku create fullstack-demo-app
heroku config:set NODE_ENV=production
git push heroku main
heroku open
heroku logs --tail
πŸ’‘ Hint β€” build fails or the page is blank

If the deploy succeeds but you see a blank page, the React build probably didn't run β€” confirm heroku-postbuild is in the root package.json and check heroku logs --tail for the build step. If the API 404s in production, make sure the catch-all app.get('*') route is defined after your /api routes.

βœ… Challenge extensions
  • Attach Postgres (heroku addons:create heroku-postgresql:essential-0) and store a record from the API.
  • Add a worker dyno and a background job to the Procfile.
  • Create a pipeline with a staging app and practice heroku pipelines:promote.
  • Enable review apps with an app.json.

🎯 Quick Quiz

Question 1: Why must a Heroku web app listen on process.env.PORT rather than a fixed port like 3000?

Question 2: Where should a Heroku app store user-uploaded files, and why?

Question 3: What does heroku pipelines:promote do?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Heroku is a PaaS: git push and a running app appears β€” infrastructure is handled for you.
  • Core concepts: dynos run processes, buildpacks compile a slug, config vars hold settings, add-ons provide data services.
  • Every app must listen on process.env.PORT and declare processes in a Procfile.
  • The filesystem is ephemeral β€” keep state in Postgres/Redis and files in S3.
  • Pipelines and review apps give you staging β†’ production with promotion and rollback.
  • Heroku has no free tier and real limits; Render, Railway, and Fly.io are close alternatives.

πŸ“š Further Reading

πŸš€ What's Next?

You've now deployed the easy way (Heroku) and the powerful way (AWS). Next we zoom in on running containers at scale: Container Orchestration Principles β€” the ideas behind Kubernetes and why they matter.

πŸŽ‰ Nicely done!

A single git push now takes you from code to a live URL. Let's scale it up next.