How to Migrate from Heroku to MicroVM Hosting
Heroku taught the industry what a deploy should feel like, and a decade later plenty of apps are still running on it happily. The migrations I see aren't ideological — they're a team looking at the monthly bill for a couple of dynos and a Postgres, comparing it to what the same compute costs elsewhere, and doing arithmetic.
The good news is that Heroku's conventions are portable by design. Twelve-factor was Heroku's idea, and an app that followed it is unusually easy to move. I'm Ajay, I build PandaStack; this guide translates each Heroku concept to its equivalent on a microVM platform, and sets out a cutover with a rollback — because the database step is the one that has consequences.
The concept mapping
- Procfile web process → your app's start command. Same string, different home.
- Procfile worker process → a separate long-running process or a scheduled function, depending on whether it's a queue consumer or periodic work.
- Config vars → environment variables, injected at build and runtime.
- Dyno → a microVM with explicit vCPU and RAM, rather than a dyno type from a menu.
- Buildpacks → repo-driven detection: a .python-version, .nvmrc, or .tool-versions file plus your dependency manifest.
- Heroku Postgres → a managed Postgres instance, with the DATABASE_URL environment variable keeping the same shape.
- Heroku Scheduler → cron schedules attached to a function.
- Review apps → per-pull-request preview deployments.
- Add-ons → this is the honest gap; see below.
Step 1: Translate the Procfile
Open your Procfile. The `web:` line is your start command, essentially verbatim — the `$PORT` variable exists on every platform worth using. If it says `bundle exec puma -C config/puma.rb`, that's what you set as your start command.
# Procfile
web: gunicorn app.wsgi --bind 0.0.0.0:$PORT --workers 3
worker: celery -A app worker --loglevel=info
release: python manage.py migrate
# Becomes:
# start command : gunicorn app.wsgi --bind 0.0.0.0:$PORT --workers 3
# worker : a second app (or process) running the celery command
# release : a build step, or a migration run as part of your deployStep 2: Move the config vars
Export them, review them, then set them on the new platform. The review matters more than the export: most long-lived Heroku apps have accumulated config vars for add-ons that were removed two years ago, and vars pointing at services you're also about to change.
# Dump the current config as JSON so you can diff it against what you set.
heroku config --json --app my-app > heroku-config.json
# Read it before you paste it. Look for:
# - vars for add-ons you no longer use
# - DATABASE_URL / REDIS_URL (these will change; don't copy them over)
# - secrets that should be rotated as part of the move anywayTreat the migration as a rotation opportunity for anything sensitive. You're touching every credential anyway, and the old values have been sitting in a platform you're leaving.
Step 3: Replace buildpacks with repo-declared runtimes
Buildpacks inspected your repo and guessed the runtime. Modern platforms do the same thing from the files a language ecosystem already uses, which has the pleasant side effect that the same file governs your laptop and production.
# Instead of a buildpack + runtime.txt:
echo "3.12" > .python-version # or .nvmrc for Node, or .tool-versions
# Dependencies come from whatever manager the repo already uses:
# requirements.txt / poetry.lock / uv.lock / Pipfile.lock
# package-lock.json / pnpm-lock.yaml / yarn.lock
# go.modpandastack app create --name my-app \
--git-url https://github.com/acme/my-app \
--start-command "gunicorn app.wsgi --bind 0.0.0.0:\$PORT --workers 3" \
--port 8000
pandastack app deploy <app-id> --followStep 4: The database — the only step with real risk
Everything above is reversible in seconds. This step isn't, so it gets a plan. There are two approaches and the right one depends entirely on how much downtime you can accept.
The simple path is a maintenance window: put the app in maintenance mode, take a final dump, restore it, point the new app at the new database, verify, and flip DNS. For most apps under a hundred gigabytes this is under an hour and vastly less complex than the alternative. Do a full dress rehearsal against a copy first — the rehearsal is where you find the extension that isn't installed on the target.
# Rehearse first, with the app still running. Then do it for real
# during the window with the app in maintenance mode.
heroku maintenance:on --app my-app
heroku pg:backups:capture --app my-app
heroku pg:backups:download --app my-app # -> latest.dump
pg_restore --no-owner --no-acl --clean --if-exists \
--dbname "$NEW_DATABASE_URL" latest.dump
# Verify BEFORE you flip anything: row counts on the tables that matter.
psql "$NEW_DATABASE_URL" -c "SELECT count(*) FROM users;"
psql "$NEW_DATABASE_URL" -c "SELECT count(*) FROM orders;"The near-zero-downtime path is logical replication: replicate Heroku Postgres to the new database, let it catch up, then stop writes briefly and cut over. It's genuinely better for a busy app and genuinely more work — worth it above a certain size, overkill below it. Be honest about which side of that line you're on rather than choosing the harder path for its own sake.
Step 5: Cut over with a rollback in hand
- Deploy to the new platform and test it thoroughly against a copy of production data, not a fresh schema.
- Lower your DNS TTL to 60 seconds at least a day ahead. This is the step that makes the rollback fast, and it must happen before you need it.
- Run the migration during your lowest-traffic window, with maintenance mode on.
- Verify with real checks: row counts, a login, a write path, a background job completing. Not just a 200 on the homepage.
- Flip DNS. Watch error rates and latency for an hour before declaring it done.
- Leave Heroku running and paid for a week. Then, and only then, tear it down.
The honest gap: add-ons
The Heroku add-on marketplace is a genuine convenience that no alternative fully replaces. A Redis add-on, a log drain, an APM, a mail service — one command each, unified billing, and they're just there. Off Heroku you'll be signing up for those services directly and wiring them yourself.
In practice this is a smaller problem than it sounds, because most of those add-ons are thin wrappers around services with their own free or cheap tiers, and the unified bill was often costing you a markup. But price the time honestly, and inventory your add-ons before you commit to a date — the surprise is never the app, it's the Redis instance nobody remembered was load-bearing.
What you actually gain
- Cost, usually the reason you started. Apps on PandaStack bill at $0.004 per vCPU-hour plus $0.008 per GiB-hour, so an always-on 2 vCPU / 4 GiB app is about $29/month, and an app that scales to zero bills nothing while asleep.
- Real resource control — explicit vCPU and RAM rather than a dyno tier, which matters most for builds and memory-hungry workers.
- Stronger isolation — each app is a Firecracker microVM with its own kernel rather than a container on a shared one.
- Managed Postgres with point-in-time recovery, cloning for migration rehearsals, and cross-host failover on the same substrate as the app.
- No sleeping-app tax — the free-tier behavior that made hobby apps take thirty seconds to wake up isn't the model here; scale-to-zero apps restore from a snapshot rather than cold-booting.
The summary
A twelve-factor Heroku app is genuinely easy to move because Heroku invented the conventions that make it portable. Translate the Procfile, review and rotate the config vars, replace buildpacks with repo-declared runtimes, and then treat the database as its own project with a rehearsal, a window, and a rollback. Lower your DNS TTL a day early, verify with real user paths rather than a homepage check, and keep the old stack alive for a week. The apps that migrate badly are the ones that skipped the rehearsal.
Frequently asked questions
How long does a Heroku migration actually take?
For a standard twelve-factor app with a Postgres under 100 GB, plan a day of preparation and a maintenance window measured in tens of minutes. The app itself usually moves in an afternoon — it's a start command and a set of environment variables. What extends the timeline is everything around the edges: add-ons that need replacing, a release phase with no obvious new home, background workers, and the database rehearsal you should do before the real thing.
Can I migrate off Heroku with zero downtime?
Yes, using logical replication: set up the new Postgres as a replica of Heroku Postgres, let it catch up, then briefly stop writes and cut over. It's the right approach for a busy production database. For most apps, though, a well-rehearsed maintenance window during low traffic is far simpler, and the honest comparison is usually twenty minutes of announced downtime versus a materially more complex procedure with more ways to go wrong.
What replaces Heroku add-ons?
You sign up for the underlying services directly — Redis from a managed provider, logging and APM from your vendor of choice, email from a transactional mail service. The convenience of one-command provisioning and a single bill genuinely goes away, and that's the real cost of leaving. The offset is that add-on pricing frequently carried a markup over going direct, so the line-item total often drops even as the number of vendor relationships goes up.
What happens to my Heroku Scheduler jobs?
They become cron schedules on the new platform. Take the opportunity to fix what Scheduler couldn't do well: Heroku Scheduler offers a limited set of intervals and no built-in alerting on missed runs. Moving to a platform with proper cron expressions and recorded run history lets you schedule off the top of the hour, see exit codes for past runs, and — most importantly — wire up an alert that fires when a job stops running at all.
Should I keep my Heroku Postgres running after migrating?
Yes, for at least a week, and don't be tempted to delete it the day you cut over to stop the billing. It's your rollback: if a data problem surfaces three days later, the original database is the only thing that lets you compare or recover. A week of Postgres charges is trivially cheap insurance against discovering a botched restore with no source to go back to.
Keep reading
- The best Heroku alternatives in 2026
- Migrating Postgres with minimal downtime
- Migrating from Vercel to microVM hosting
- Zero-downtime schema migrations on deploy
- PandaStack pricing — per-second billing, apps at $0.004/vCPU-hr
49ms p50 cold start. Fork, snapshot, and scale to zero.