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
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.,
PORTenv 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-serverStep 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/myappRemove 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.dumpStep 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.targetStep 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.comStep 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.