Introduction to Heroku
In our previous lectures, we explored various cloud providers and delved into AWS deployment strategies. Now, we'll focus on Heroku, a Platform as a Service (PaaS) that offers a simpler, more developer-friendly approach to deploying applications.
Heroku abstracts away much of the infrastructure complexity, allowing developers to focus on writing code rather than managing servers. Think of Heroku as a concierge service for your applications—you provide the code, and Heroku handles the infrastructure, scaling, monitoring, and other operational aspects.
Why Choose Heroku?
- Simplicity: Deploy with a simple Git push
- Developer Experience: Focus on code, not infrastructure
- Quick Prototyping: Rapid deployment for MVPs and prototypes
- Managed Services: Easy integration with databases and other services
- Built-in DevOps: CI/CD, logging, and monitoring included
- Free Tier: Ideal for learning and small projects
When Heroku Might Not Be the Best Choice
- Cost at Scale: Can be more expensive than IaaS for large applications
- Limited Control: Less flexibility in infrastructure configuration
- Vendor Lock-in: Some Heroku-specific features don't transfer to other platforms
- Geographical Limitations: Fewer regions than major cloud providers
- Complex Architecture: May not be suitable for highly complex applications
Heroku Architecture
Core Concepts
Understanding Heroku's core concepts is essential for effective deployment:
Applications
A Heroku application is a collection of dynos, configuration, and add-ons that together run your code.
Dynos
Dynos are lightweight Linux containers that run your application code. Heroku offers several dyno types:
- Web Dynos: Run web servers and respond to HTTP requests
- Worker Dynos: Run background jobs and process queues
- One-off Dynos: Run administrative tasks like database migrations
Buildpacks
Buildpacks are scripts that prepare your code for execution. They detect the language and framework of your application and install the necessary dependencies.
- Official Buildpacks: Node.js, Ruby, Python, Java, PHP, Go, etc.
- Third-party Buildpacks: Community-developed for additional languages and frameworks
- Custom Buildpacks: Create your own for specific requirements
Slugs
A slug is a compressed and packaged copy of your application optimized for distribution to the dyno manager. The buildpack compiles your application into a slug during the build process.
Add-ons
Add-ons are third-party services that extend your application's functionality:
- Databases: PostgreSQL, MySQL, MongoDB, Redis
- Monitoring: New Relic, Scout, Librato
- Logging: Papertrail, Logentries
- Caching: Memcached, Redis
- Search: Elasticsearch, Algolia
- And many more: Email, SMS, PDF generation, etc.
Config Vars
Config vars are environment variables that allow you to customize your application's behavior without modifying code.
Release
A release is a snapshot of your application code, config vars, and add-ons. Heroku maintains a history of releases, allowing you to roll back if needed.
Deploying to Heroku
Preparation for Deployment
Before deploying to Heroku, ensure your application meets these requirements:
Language Support
Heroku supports many languages and frameworks through buildpacks:
- Node.js
- Ruby
- Python
- Java
- PHP
- Go
- Scala
- Clojure
Essential Files
Depending on your language, you'll need specific files:
- Node.js: package.json with start script
- Python: requirements.txt and Procfile
- Ruby: Gemfile and Procfile
- Java: pom.xml or build.gradle
- PHP: composer.json
- Go: go.mod
Procfile
A Procfile specifies the commands that are executed by the app on startup:
# Example Procfile for Node.js
web: node server.js
# Example Procfile for Python
web: gunicorn app:app
# Example Procfile with multiple process types
web: node server.js
worker: node worker.js
Port Configuration
Your application must listen on the port specified by the PORT environment variable:
// Node.js example
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
# Python Flask example
from flask import Flask
import os
app = Flask(__name__)
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)
Deployment Methods
Heroku offers several ways to deploy your application:
Git Deployment
The most common method is deploying directly from Git:
# Create a new Heroku app
heroku create my-awesome-app
# Deploy your code
git push heroku main
# Open your application in a browser
heroku open
GitHub Integration
You can connect your GitHub repository to Heroku for automated deployments:
- Go to your Heroku Dashboard
- Select your app
- Go to the "Deploy" tab
- Select "GitHub" as the deployment method
- Connect to your GitHub repository
- Enable automatic deploys (optional)
Container Registry
For Docker-based deployments, you can use Heroku Container Registry:
# Log in to the Heroku Container Registry
heroku container:login
# Build and push your Docker image
docker build -t registry.heroku.com/my-app/web .
docker push registry.heroku.com/my-app/web
# Release the image to your app
heroku container:release web -a my-app
Heroku CLI Deployment
You can deploy directly using the Heroku CLI:
# Deploy from a specific directory
heroku deploy:src
# Deploy a WAR file (Java)
heroku deploy:war --war target/myapp.war
Managing Environment Variables
Config vars in Heroku allow you to customize your application's behavior:
# Set a config var
heroku config:set DATABASE_URL=postgres://username:password@host:port/database
# View all config vars
heroku config
# Remove a config var
heroku config:unset API_KEY
Managing Add-ons
Heroku add-ons extend your application's functionality:
# Add PostgreSQL database
heroku addons:create heroku-postgresql:hobby-dev
# Add Redis for caching
heroku addons:create heroku-redis:hobby-dev
# Add Papertrail for logging
heroku addons:create papertrail:choklad
# List all add-ons
heroku addons
# Remove an add-on
heroku addons:destroy heroku-redis
Heroku Deployment Examples
Node.js Application
Deploying a typical Node.js/Express application:
Required Files
# package.json
{
"name": "nodejs-example",
"version": "1.0.0",
"description": "Node.js Example for Heroku",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2"
},
"engines": {
"node": "18.x"
}
}
# server.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Hello from Heroku!');
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Deployment Steps
# Initialize Git repository (if not already done)
git init
git add .
git commit -m "Initial commit"
# Create Heroku app
heroku create nodejs-example-app
# Deploy to Heroku
git push heroku main
# Scale to 1 web dyno (default)
heroku ps:scale web=1
# Open the app in browser
heroku open
Python Flask Application
Deploying a Flask application to Heroku:
Required Files
# 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==2.2.3
gunicorn==20.1.0
# Procfile
web: gunicorn app:app
Deployment Steps
# Create a virtual environment (local development)
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install flask gunicorn
# Generate requirements.txt
pip freeze > requirements.txt
# Initialize Git and commit
git init
git add .
git commit -m "Initial commit"
# Create Heroku app
heroku create flask-example-app
# Deploy to Heroku
git push heroku main
# Open the app
heroku open
React + Node.js Full-Stack Application
Deploying a full-stack application with React frontend and Node.js backend:
Project Structure
my-fullstack-app/
├── client/ # React frontend
│ ├── public/
│ ├── src/
│ ├── package.json
│ └── ...
├── server/ # Node.js backend
│ ├── server.js
│ └── ...
├── package.json # Root package.json
└── ...
Root package.json
{
"name": "fullstack-example",
"version": "1.0.0",
"engines": {
"node": "18.x"
},
"scripts": {
"start": "node server/server.js",
"build": "cd client && npm install && npm run build",
"dev": "concurrently \"npm run server\" \"npm run client\"",
"server": "nodemon server/server.js",
"client": "cd client && npm start",
"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"concurrently": "^7.6.0",
"nodemon": "^2.0.21"
}
}
Server Code
// server/server.js
const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 5000;
// API routes
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hello from the backend!' });
});
// Serve static assets in production
if (process.env.NODE_ENV === 'production') {
// Set static folder
app.use(express.static(path.join(__dirname, '../client/build')));
// Any route that's not an API route will be handled by the React app
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, '../client/build', 'index.html'));
});
}
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Deployment Steps
# Initialize Git repository
git init
git add .
git commit -m "Initial commit"
# Create Heroku app
heroku create fullstack-example-app
# Set environment variables
heroku config:set NODE_ENV=production
# Deploy to Heroku
git push heroku main
# Open the app
heroku open
Database-Backed Application
Deploying an application with a PostgreSQL database:
Adding a Database
# Add PostgreSQL add-on
heroku addons:create heroku-postgresql:hobby-dev
# Check the database URL
heroku config:get DATABASE_URL
# Connect to the database
heroku pg:psql
Database Connection Code (Node.js/Sequelize)
// db.js
const { Sequelize } = require('sequelize');
// Extract database configuration from DATABASE_URL
const databaseUrl = process.env.DATABASE_URL || 'postgres://localhost:5432/local_db';
// Configure SSL for Heroku PostgreSQL
const sequelize = new Sequelize(databaseUrl, {
dialect: 'postgres',
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false // Required for Heroku PostgreSQL
}
}
});
// Test the connection
async function testConnection() {
try {
await sequelize.authenticate();
console.log('Database connection has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
}
testConnection();
module.exports = sequelize;
Database Migrations
# Run migrations on Heroku
heroku run npx sequelize-cli db:migrate
# Seed the database
heroku run npx sequelize-cli db:seed:all
Advanced Heroku Topics
Heroku Pipelines
Heroku Pipelines provide a continuous delivery workflow for your applications:
Setting Up a Pipeline
# Create a pipeline
heroku pipelines:create my-pipeline --stage staging -a my-staging-app
# Add an app to a pipeline
heroku pipelines:add my-pipeline -a my-production-app --stage production
# Promote an app from staging to production
heroku pipelines:promote -a my-staging-app
Review Apps
Review Apps create temporary apps for each GitHub pull request:
- Go to your pipeline dashboard
- Enable Review Apps
- Configure the app.json file in your repository
# app.json example
{
"name": "My Application",
"description": "My application description",
"repository": "https://github.com/username/repo",
"env": {
"NODE_ENV": {
"value": "review"
}
},
"addons": [
"heroku-postgresql:hobby-dev"
],
"buildpacks": [
{
"url": "heroku/nodejs"
}
],
"scripts": {
"postdeploy": "npm run db:migrate"
}
}
Custom Domains & SSL
Using custom domains with your Heroku apps:
# Add a custom domain
heroku domains:add www.example.com
# Verify domain ownership
heroku domains:add example.com
# Automatically manage SSL certificates
heroku certs:auto:enable
# View domain status
heroku domains
Then update your DNS settings with your domain provider:
- Create a CNAME record for www pointing to your Heroku app's domain
- Set up ALIAS/ANAME record for root domain (or use domain forwarding)
Scaling Applications
Scaling your application horizontally and vertically:
# Scale horizontally (add more dynos)
heroku ps:scale web=3 worker=2
# Scale vertically (use different dyno types)
heroku ps:resize web=standard-2x
# View current dyno configuration
heroku ps
# View resource usage
heroku ps:utilization
Monitoring & Logging
Viewing logs and monitoring your application:
# View real-time logs
heroku logs --tail
# Filter logs by source
heroku logs --source app --tail
# Add Papertrail add-on for advanced logging
heroku addons:create papertrail:choklad
# Add New Relic for monitoring
heroku addons:create newrelic:wayne
# View application metrics
heroku metrics
Heroku Scheduler
Running scheduled tasks with Heroku Scheduler:
# Add Scheduler add-on
heroku addons:create scheduler:standard
# Open Scheduler dashboard
heroku addons:open scheduler
In the Scheduler dashboard, you can add jobs that run at specified intervals:
- Daily
- Hourly
- Every 10 minutes
Example commands for scheduled jobs:
node scripts/daily-report.jspython manage.py clearsessionsphp artisan schedule:run
Heroku Deployment Best Practices
Application Architecture
- Stateless Application Design: Don't rely on local file storage
- 12-Factor App Methodology: Follow principles for cloud-native applications
- Microservices: Consider breaking large applications into smaller services
- Background Processing: Use worker dynos for long-running tasks
- Caching: Implement caching for improved performance
Performance Optimization
- Asset Optimization: Compress and minimize static assets
- CDN Integration: Use Cloudflare or similar for static content
- Database Indexing: Optimize database queries with proper indexes
- Connection Pooling: Manage database connections efficiently
- Memory Management: Monitor and optimize memory usage
Security Considerations
- Environment Variables: Store secrets as config vars, not in code
- Authentication: Implement proper authentication and authorization
- SSL: Use HTTPS for all traffic
- Dependency Management: Keep dependencies updated
- Security Headers: Implement appropriate security headers
Deployment Workflow
- CI/CD Integration: Automate testing and deployment
- Pipelines: Use Heroku Pipelines for staging and production environments
- Review Apps: Test changes in isolated environments before merging
- Deployment Verification: Verify deployments with smoke tests
- Rollbacks: Be prepared to quickly roll back problematic deployments
Monitoring and Maintenance
- Logging Strategy: Implement structured logging
- Alerting: Set up alerts for critical issues
- Performance Monitoring: Track application performance metrics
- Database Maintenance: Regularly maintain and optimize databases
- Backup Strategy: Set up regular database backups
Real-World Heroku Examples
Example 1: SaaS Web Application
A multi-tier SaaS application deployed on Heroku:
React + Node.js] E --> G[Worker Dynos
Background Jobs] E --> H[Scheduler
Periodic Tasks] F & G & H --> I[PostgreSQL Database] F & G & H --> J[Redis Cache] K[Custom Domain
example.com] --> E L[Monitoring
New Relic] --> E M[Logging
Papertrail] --> E
Key Components:
- React frontend and Node.js backend in web dynos
- Background processing in worker dynos
- PostgreSQL for data storage
- Redis for caching and job queues
- Continuous deployment with Heroku Pipelines
- Multiple environments (review, staging, production)
- Custom domain with SSL
- Monitoring and logging add-ons
Example 2: Content Management System
A WordPress site deployed on Heroku:
Media Storage] E[Redis Cache] --> B F[Cloudflare CDN] --> B G[SendGrid
Email Service] --> B H[Custom Theme] --> B I[Custom Plugins] --> B
Implementation Details:
- WordPress running on PHP buildpack
- PostgreSQL for database (using a WordPress plugin for PostgreSQL support)
- S3 for media storage since Heroku's filesystem is ephemeral
- Redis for object caching
- Cloudflare for CDN and additional caching
- SendGrid for reliable email delivery
- Custom theme and plugins stored in the repository
Example 3: Microservices Architecture
A system with multiple interconnected services on Heroku:
Node.js] --> B[User Service
Python] A --> C[Product Service
Node.js] A --> D[Order Service
Java] A --> E[Payment Service
Ruby] B & C & D & E --> F[Service Registry] B --> G[User Database] C --> H[Product Database] D --> I[Order Database] E --> J[Payment Database] K[Message Queue
RabbitMQ] --> B & C & D & E
Architecture Notes:
- Multiple Heroku apps for different services
- Each service has its own database
- CloudAMQP (RabbitMQ as a service) for message queuing
- API Gateway for routing requests to appropriate services
- Mix of programming languages based on team expertise
- Service discovery for inter-service communication
- Independently deployable and scalable services
Limitations and Alternatives
Heroku Limitations
While Heroku is great for many applications, it has some limitations:
- Ephemeral Filesystem: Files written to the filesystem are temporary
- Dyno Sleeping: Free-tier dynos sleep after 30 minutes of inactivity
- Limited Request Timeout: 30-second request timeout
- Maximum Request/Response Size: Limited to 1MB for the Router
- Language Constraints: Limited to supported languages/frameworks
- Add-on Limitations: Some add-ons have usage limits
- Cost at Scale: Can be more expensive than IaaS at large scale
- Geographic Availability: Limited regions compared to major cloud providers
Alternative PaaS Providers
If Heroku doesn't meet your needs, consider these alternatives:
| Provider | Key Features | Best For |
|---|---|---|
| Render | Simple deployments, free tier, native support for many languages | Modern web applications, startups |
| DigitalOcean App Platform | Simple pricing, integrated with DigitalOcean services | Developers familiar with DigitalOcean |
| Google App Engine | Automatic scaling, integration with Google Cloud | Applications needing deep Google Cloud integration |
| Azure App Service | Windows and Linux options, enterprise integration | Enterprise applications, .NET applications |
| AWS Elastic Beanstalk | AWS integration, more control over infrastructure | Applications needing deeper AWS integration |
| Fly.io | Global deployment, persistent volumes, WebSockets | Applications needing global presence |
| Railway | Developer-friendly, GitHub integration, persistent storage | Developer projects, startups |
When to Consider Moving from Heroku
Signs that you might need to look beyond Heroku:
- Cost Concerns: Monthly bills becoming too high for your budget
- Performance Needs: Requiring more customized infrastructure
- Compliance Requirements: Needing specific compliance certifications
- Geographic Requirements: Requiring presence in regions Heroku doesn't support
- Technology Constraints: Using technologies not well-supported by Heroku
- Complex Architecture: Outgrowing Heroku's simplified architecture model
Migration Strategies
If you need to migrate from Heroku, consider these approaches:
- Containerization: Convert your application to Docker for portability
- Infrastructure as Code: Define your infrastructure with tools like Terraform
- Staged Migration: Move one component at a time
- Hybrid Approach: Keep some components on Heroku, move others to different platforms
- Platform Selection: Choose a platform that minimizes migration effort
Practical Exercise: Deploying to Heroku
Exercise: Deploy a Full-Stack Application to Heroku
In this exercise, you'll deploy a full-stack application (React frontend + Express backend) to Heroku.
Prerequisites
- Node.js and npm installed locally
- Git installed
- Heroku CLI installed
- Heroku account
Step 1: Create a Simple Full-Stack Application
# Create project directory
mkdir heroku-fullstack-demo
cd heroku-fullstack-demo
# Initialize Git
git init
# Create package.json
npm init -y
# Install dependencies
npm install express cors
# Create a simple server file
cat > server.js << 'EOF'
const express = require('express');
const path = require('path');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// API route
app.get('/api/info', (req, res) => {
res.json({
message: 'Hello from the backend!',
env: process.env.NODE_ENV,
timestamp: new Date()
});
});
// Serve static assets in production
if (process.env.NODE_ENV === 'production') {
app.use(express.static('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}`));
EOF
# Update package.json scripts
npm pkg set scripts.start="node server.js"
npm pkg set scripts.heroku-postbuild="cd client && npm install && npm run build"
# Create client React app
npx create-react-app client
# Update client src/App.js
cat > client/src/App.js << 'EOF'
import React, { useState, useEffect } from 'react';
import './App.css';
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Determine API URL based on environment
const apiUrl = process.env.NODE_ENV === 'production'
? '/api/info'
: 'http://localhost:5000/api/info';
fetch(apiUrl)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
console.error('Error fetching data:', error);
setLoading(false);
});
}, []);
return (
Full-Stack Heroku Deployment
{loading ? (
Loading data...
) : data ? (
Message from server: {data.message}
Environment: {data.env}
Timestamp: {new Date(data.timestamp).toLocaleString()}
) : (
No data received from server
)}
);
}
export default App;
EOF
# Create Procfile
echo "web: npm start" > Procfile
# Create .gitignore
cat > .gitignore << 'EOF'
node_modules
.env
.DS_Store
npm-debug.log
client/node_modules
client/build
EOF
# Commit changes
git add .
git commit -m "Initial commit"
Step 2: Deploy to Heroku
# Login to Heroku
heroku login
# Create a new Heroku app
heroku create fullstack-demo-app
# Set environment variables
heroku config:set NODE_ENV=production
# Push to Heroku
git push heroku main
# Open the app
heroku open
# View logs
heroku logs --tail
Step 3: Add a Database
# Add PostgreSQL database
heroku addons:create heroku-postgresql:hobby-dev
# Update server.js to use the database
# (Not included in this example for brevity)
# Commit and push changes
git add .
git commit -m "Add database support"
git push heroku main
Step 4: Configure a Pipeline
# Create a pipeline
heroku pipelines:create fullstack-pipeline --stage production -a fullstack-demo-app
# Create a staging app
heroku create fullstack-demo-staging --remote staging
# Add staging app to pipeline
heroku pipelines:add fullstack-pipeline -a fullstack-demo-staging --stage staging
# Push to staging
git push staging main
# Promote from staging to production
heroku pipelines:promote -a fullstack-demo-staging
Challenge Extensions
- Add a worker dyno for background processing
- Implement a Redis cache
- Set up a custom domain
- Enable review apps in your pipeline
- Add monitoring with the New Relic add-on
Conclusion
Heroku provides a developer-friendly platform for deploying applications with minimal infrastructure management. Its simplicity, integrated services, and streamlined workflow make it an excellent choice for many web applications, particularly for startups, small teams, and projects where development speed is a priority.
Key takeaways from this lecture include:
- Simplicity and Speed: Heroku enables rapid deployment with minimal configuration
- Managed Services: Add-ons provide easy integration with databases, caching, and other services
- Deployment Options: Multiple ways to deploy, including Git, GitHub integration, and container registry
- Pipelines and Workflow: Built-in tools for continuous delivery and testing
- Limitations: Understanding when Heroku is appropriate and when you might need alternatives
Whether you're deploying a simple web application, a complex microservices architecture, or anything in between, Heroku offers a platform that lets you focus on your code rather than infrastructure management.