← Back to Blog
MigrationTutorial

Migrating from Heroku to Bare Metal: Step-by-Step Guide

Heroku’s free tier is gone, dyno pricing keeps climbing, and you are paying $50+/mo for resources you could get for $6 on bare metal. Here is how to migrate your entire Heroku app — database, environment variables, and all — without downtime.

Why Leave Heroku

Heroku was the gold standard for developer experience. Then Salesforce acquired it, killed the free tier, raised prices, and let the platform stagnate. In 2026, a Basic dyno costs $7/mo with 512 MB RAM and shared CPU. A Heroku Postgres Mini is another $5/mo for 10K rows.

For $6/mo on RAW, you get a dedicated bare metal server with 2 CPU cores, 4 GB RAM, 40 GB NVMe, and 20 TB bandwidth. That is enough to run your app, database, and cache on a single machine — with room to spare.

Cost Comparison

ComponentHerokuRAW Bare Metal
App server$7–$50/mo (dyno)$6/mo (full server)
PostgreSQL$5–$50/mo (add-on)$0 (self-hosted)
Redis$15–$60/mo (add-on)$0 (self-hosted)
SSLIncludedFree (Let’s Encrypt)
Typical total$27–$160/mo$6/mo

Migration Checklist

Before you start, gather everything you need from Heroku:

  • Export all environment variables (heroku config)
  • Create a database backup
  • List all add-ons and find bare metal alternatives
  • Document your Procfile commands
  • Note custom domains and DNS settings
  • Check for Heroku-specific code (e.g., PORT env var, Heroku headers)

Step 1: Deploy Your RAW Server

# Deploy a bare metal server
npx rawhq deploy

# SSH in
ssh root@your-server-ip

# Install your runtime (Node.js example)
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs

# Install PostgreSQL and Redis
apt install -y postgresql redis-server

Step 2: Export Environment Variables

Heroku stores config in environment variables. Export them all:

# On your local machine
heroku config -a your-app-name --shell > .env.production

# Review and update database URLs
# Old: DATABASE_URL=postgres://user:pass@ec2-xx.amazonaws.com:5432/dbname
# New: DATABASE_URL=postgres://app:yourpass@localhost:5432/myapp

Remove Heroku-specific variables like HEROKU_APP_NAME and HEROKU_SLUG_COMMIT. Update any add-on URLs (Redis, Elasticsearch, etc.) to point to your local services.

Step 3: Export and Import the Database

Create a Heroku backup and import it on your bare metal server:

# Create a fresh backup on Heroku
heroku pg:backups:capture -a your-app-name

# Download the backup
heroku pg:backups:download -a your-app-name
# This creates latest.dump in your current directory

# Copy to your server
scp latest.dump root@your-server-ip:/tmp/

# On the server: create the database and import
sudo -u postgres createuser app
sudo -u postgres createdb -O app myapp
pg_restore --verbose --clean --no-acl --no-owner \
  -U app -d myapp /tmp/latest.dump

Step 4: Deploy Your Application

Your Heroku Procfile tells you exactly what commands to run. Translate them to systemd or PM2:

# If your Procfile says: web: node server.js
# Option A: PM2 (Node.js apps)
npm install -g pm2
cd /opt/myapp
npm install --production
pm2 start server.js --name myapp
pm2 save
pm2 startup

# Option B: systemd (any language)
# Create /etc/systemd/system/myapp.service
# [Unit]
# Description=My App
# After=postgresql.service
# [Service]
# Type=simple
# User=app
# WorkingDirectory=/opt/myapp
# EnvironmentFile=/opt/myapp/.env.production
# ExecStart=/usr/bin/node server.js
# Restart=always
# [Install]
# WantedBy=multi-user.target

Step 5: Set Up Nginx and SSL

# Install Nginx
apt install -y nginx

# Configure reverse proxy
cat > /etc/nginx/sites-available/myapp <<'EOF'
server {
    listen 80;
    server_name yourdomain.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF

ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

# Add SSL with Certbot
apt install -y certbot python3-certbot-nginx
certbot --nginx -d yourdomain.com

Step 6: DNS Cutover

The final step. Minimize downtime by preparing everything first:

  • Lower your DNS TTL to 60 seconds 24 hours before the migration
  • Verify your app works on the bare metal server using the IP address directly
  • Update your DNS A record to point to the RAW server IP
  • Wait for propagation (check with dig yourdomain.com)
  • Keep Heroku running for 24–48 hours as a fallback
  • Once confirmed, scale Heroku dynos to zero and delete the app

Heroku-Specific Code to Update

Heroku injects a PORT environment variable. Most frameworks already read this, but verify your app binds to the right port. Other Heroku patterns to check:

  • process.env.PORT — Set this in your .env or systemd config
  • Heroku-specific headers like X-Request-Id — Nginx does not add these by default
  • Ephemeral filesystem — On bare metal, your disk is persistent. Move file uploads from S3 to local storage if you prefer
  • Heroku scheduler — Replace with cron jobs on your server

Post-Migration Checklist

  • SSL certificate is valid and auto-renewing
  • Database backups are scheduled (daily pg_dump via cron)
  • Application restarts on crash (systemd or PM2)
  • Monitoring is set up (uptime checks, disk space alerts)
  • Firewall is configured (UFW: allow 22, 80, 443 only)
  • Heroku app is decommissioned to stop billing

Get Started

Deploy a RAW server, migrate your Heroku app, and save 80%+ on hosting. The entire process takes under an hour for most applications.