I Mass-Migrated 14 Projects From Heroku to Railway in One Weekend — Here’s Every Mistake I Made

“`html

Disclosure: This article contains affiliate links. If you purchase through these links, we may earn a commission at no extra cost to you. Our contributors’ opinions are their own and are never influenced by affiliate relationships.

I Mass-Migrated 14 Projects From Heroku to Railway in One Weekend — Here’s Every Mistake I Made

I’ve been on Heroku since 2016. Back then it was magic — git push heroku main and your app was live. No Docker, no YAML files, no infrastructure degree required. I built 14 side projects, internal tools, and client prototypes on it over the years, all sitting on various dyno tiers.

Then Heroku killed the free tier in November 2022. I told myself I’d migrate “next month.” That was two years ago. I was paying $147/month for projects that collectively got maybe 200 requests per day. One of them was a webhook receiver I built for a client who went out of business in 2023. Still running. Still billing.

Last month I finally blocked out a weekend, made a pot of coffee, and migrated all 14 projects to Railway. Here’s exactly what happened.

Why Railway and Not Something Else

I looked at five alternatives seriously:

Render was the obvious choice. Everyone recommends it as the “Heroku replacement.” I tried it for one project six months ago and hit two issues: cold starts on the free tier were brutal (15+ seconds), and their build system choked on a monorepo with a shared TypeScript package. Maybe they’ve fixed it since, but I didn’t want to fight it again.

Fly.io is technically impressive but I didn’t want to learn another deployment mental model. Fly thinks in terms of machines and volumes and regions. I think in terms of “here’s my app, run it.” That’s what I liked about Heroku in the first place.

DigitalOcean App Platform was tempting since I already use DO droplets for other things. But the pricing felt weird for small apps — the $5/month starter has limited build minutes and I knew I’d burn through them fast with 14 projects.

Vercel/Netlify are great for frontends but half my projects are backend services, cron jobs, and worker processes. Not the right fit.

Railway won because it felt the most like Heroku. Connect a GitHub repo, it detects the language, builds it, deploys it. Environment variables in a dashboard. Logs that actually work. And the pricing model — you pay for what you use, with a $5/month base — meant my low-traffic hobby projects would cost almost nothing.

The Migration Spreadsheet

Before touching anything, I made a spreadsheet of all 14 projects:

Project Stack Database Addons Monthly Cost (Heroku)
webhook-relay Node.js/Express None None $7
invoice-generator Python/Flask Postgres Redis $16
link-shortener Node.js Postgres None $7
client-dashboard React + Node API Postgres None $14
email-parser Python None Cloudinary $7
metrics-collector Go Postgres None $7
… (8 more) Various Various Various $89

Total Heroku bill: $147/month for apps that barely get traffic. Embarrassing.

What Went Smoothly

8 out of 14 projects migrated in under 10 minutes each. The simple Node.js and Python apps with no database were trivial. Connect the GitHub repo to Railway, add the environment variables, deploy. Done. Railway even auto-detected the right buildpack for each one.

The Procfile format is identical. If your Heroku app has web: node server.js, Railway reads the same file. I didn’t change a single line of code for these eight projects.

Railway’s CLI is actually better than Heroku’s. railway link to connect a local directory to a project, railway run to execute commands with production env vars, railway logs to tail logs. It’s faster and the output is cleaner.

Environment variable management is nicer. Railway has a proper UI for env vars with different values per environment (staging vs production). On Heroku I was always mixing up which vars were set on which app.

What Broke Badly

Mistake 1: Postgres Migration Without Testing Locally

I used pg_dump from the Heroku Postgres addon and tried to pg_restore into Railway’s Postgres. The first three databases imported fine. The fourth one — my invoice generator with 18 months of data — failed silently. The restore appeared to complete, but half the tables had zero rows.

The problem: Heroku’s pg_dump was outputting in a format that skipped some tables due to permission issues on their shared Postgres. I didn’t notice because pg_restore doesn’t error on skipped data by default.

The fix: I switched to dumping with --no-owner --no-privileges --format=plain and importing with psql directly instead of pg_restore. Every row came through. But I lost two hours figuring this out and had a mild panic attack thinking I’d lost client invoice data.

Lesson: Always run SELECT COUNT(*) FROM every_table before and after migration. Don’t trust the tools to tell you something went wrong.

Mistake 2: Hardcoded Heroku URLs Everywhere

Three of my projects had hardcoded https://my-app.herokuapp.com URLs in places I’d completely forgotten about:

  • A webhook URL registered with a third-party API
  • CORS configuration allowing only the Heroku domain
  • An OAuth redirect URI in Google Cloud Console

Each one caused a different confusing error. The webhook one was the worst — the third-party service was silently dropping requests to the old URL, and I didn’t realize for six hours that my production webhook handler wasn’t receiving data.

Lesson: Before migrating, grep your entire codebase for herokuapp.com. Also check every third-party dashboard where you’ve registered URLs.

Mistake 3: Redis Connection Strings

Two projects used Heroku Redis. Railway has its own Redis addon, but the connection string format is slightly different. Heroku uses REDIS_URL with a redis:// scheme. Railway’s Redis plugin sets REDIS_URL too, but I had one project that was reading REDIS_TLS_URL for TLS connections, which Railway doesn’t set by default.

Took me 45 minutes to debug a “connection refused” error that was just a missing environment variable.

Mistake 4: The Cron Job That Ran Twice

One of my projects is a daily email digest that runs via a scheduled task. On Heroku, I used Heroku Scheduler. Railway has a built-in cron feature, which is actually better. But during migration, I set up the Railway cron before turning off the Heroku Scheduler.

For one glorious day, my users got two copies of every digest email. Three people replied asking if they were being hacked. I was not.

The Final Bill

Platform Monthly Cost
Heroku (before) $147
Railway (after) $23
Savings $124/month ($1,488/year)

The biggest savings came from Railway’s usage-based pricing. My low-traffic apps cost literal cents per month instead of $7/month minimum on Heroku. The invoice generator with its Postgres database is the most expensive at about $8/month. Everything else is under $2.

Would I Recommend Railway?

Yes, with caveats.

If you’re running straightforward web services and APIs, Railway is what Heroku should have become. The developer experience is genuinely great, the pricing is fair, and the migration from Heroku is easier than any other platform I tried.

If you need advanced networking, multi-region deployments, or you’re running something with serious compliance requirements, look at Fly.io or go straight to a cloud provider with managed containers. DigitalOcean’s managed infrastructure is worth a look if you’re already in that ecosystem and need more control than a PaaS gives you.

If you’re paying Heroku tax on hobby projects you forgot about, just do the migration. Block out a Saturday. Make the spreadsheet. You’ll be done by dinner and you’ll stop burning $100+/month on apps nobody uses.

Two years of procrastination cost me $3,528 in unnecessary Heroku bills. The migration took 11 hours including all the mistakes. Don’t be like me.

“`

Leave a Comment

Stay sharp.

A weekly breakdown of the tools worth your time — and the ones that aren't.

Join 500+ developers. No spam ever.