Migrating From Heroku to Deploynix: A Cost and Control Upgrade for Laravel
There's a specific moment when a Heroku invoice stops making sense. For most teams it isn't the first month, or even the first year. It's the month you scale a dyno to handle real traffic, add a worker for your queue, bump Postgres off the hobby tier, and realize you're paying north of $100 every month for compute that a single mid-range VPS would handle without breaking a sweat. You stare at the line items, do the mental math, and the number just doesn't add up anymore.
We've heard this story from Laravel developers repeatedly over the past two years, and the data backs it up. In a Barclays CIO survey, 83% of enterprises said they plan to repatriate at least some workloads from public cloud back to infrastructure they control (Northflank, 2024). Organizations that make the move strategically report infrastructure savings of 30-60% (MassiveGRID, 2026). The pendulum that swung hard toward "never touch a server" is swinging back toward "own your stack, but automate the painful parts."
To be clear: Heroku earned its reputation. git push heroku main was a genuine revolution in 2010, and the developer experience is still excellent in 2026. The problem was never the product. The problem is the price you pay for that experience once your app is no longer a side project, and the ceiling you hit when you need control Heroku doesn't offer.
This guide is the complete migration path for a production Laravel application: why teams leave, how every Heroku concept maps to a VPS, a pre-migration audit checklist, a step-by-step runbook with real commands, a worked cost comparison, and an honest accounting of what you take on when you leave a fully managed platform.
Key Takeaways
- A typical production Heroku stack (web dyno, worker dyno, Postgres, Redis) runs about $115/month; a comparable VPS costs $12-24/month. - 83% of enterprises plan to repatriate cloud workloads (Northflank, 2024). - Heroku uses PostgreSQL, and Deploynix supports PostgreSQL natively, so no database engine switch is required. - A parallel-running window plus a low-TTL DNS cutover makes the migration effectively zero-downtime.
Why Are Teams Leaving Heroku in 2026?
Cost at scale is the headline reason, and it's substantial. Strategic cloud repatriation delivers 30-60% infrastructure savings on average (MassiveGRID, 2026), and Heroku migrations often land at the top of that range because dyno pricing carries the platform's largest margins. But cost is only the most visible of four recurring complaints.
The Cost Curve Bends the Wrong Way
Heroku's pricing is friendly at the bottom and punishing at the top. At the time of writing, a Basic dyno runs about $7/month, a Standard-1X about $25/month, and Performance dynos start at $250/month and climb from there. Managed Postgres and the Key-Value Store (Redis-compatible) are priced separately, each with their own tier ladder.
The trouble is that these tiers don't scale with hardware prices. VPS compute has gotten dramatically cheaper per gigabyte of RAM over the past decade. Heroku's dyno pricing hasn't followed. A Standard-1X dyno gives you 512MB of RAM for $25/month. A $9/month VPS from Hetzner gives you 4GB. That's roughly a 20x difference in price per gigabyte, and it compounds across every dyno and addon you run.
Dyno Constraints
Dynos restart daily, whether you want them to or not. The ephemeral filesystem means anything written to disk vanishes on restart, which forces you onto S3 for every uploaded file even when local storage would be fine. Memory limits are hard walls: exceed 512MB on a Standard-1X and you get R14 errors and degraded performance until you pay for the next tier. None of these constraints exist on a server you control.
Addon Sprawl
Every capability is a separate line item. Postgres is an addon. Redis is an addon. Log retention beyond 1,500 lines is an addon. Scheduled jobs, monitoring, error tracking: addons. Each one is individually reasonable and collectively expensive. Teams routinely discover their addon spend exceeds their dyno spend, and that a lot of it duplicates things Laravel and a VPS give you for free.
EU Data Residency
Heroku's Common Runtime offers US and EU regions, but fine-grained control over where your data physically lives requires Private Spaces, which start at enterprise-level pricing. For teams with GDPR obligations or customers who ask pointed questions about data location, "our database is on a Hetzner server in Falkenstein, Germany" is a much easier answer than a diagram of Heroku's addon infrastructure. This is a pattern we cover across the whole PaaS category in why Laravel developers are leaving generic PaaS platforms, and it applies to Heroku more than most.
How Do Heroku Concepts Map to a VPS?
Every Heroku primitive has a direct equivalent on a Deploynix-managed server, which is what makes this migration mechanical rather than architectural. Nothing in your Laravel codebase needs to change. The table below is the mental model for the entire migration; the rest of this guide is just executing it row by row.
Heroku concept | Deploynix / VPS equivalent |
|---|---|
Dyno | Server + site (Nginx + PHP-FPM) |
| Supervisor-managed daemons and queue workers, configured in the UI |
Config vars |
|
Release phase ( | Deploy hooks that run during each deployment |
Heroku Postgres addon | Self-hosted PostgreSQL on your server |
Heroku Key-Value Store (Redis) | Valkey (Redis-compatible), installed by default |
Heroku Scheduler addon | Laravel scheduler via cron ( |
| Server monitoring with CPU, RAM, and disk alerts |
| One-click instant rollback to a previous release |
Automatic SSL | Free Let's Encrypt SSL, auto-renewed |
Two rows deserve emphasis. First, the database: Heroku standardized on PostgreSQL, and Deploynix provisions PostgreSQL alongside MySQL and MariaDB, so there's no forced engine migration. Your pg_dump restores directly. Second, the release phase: Heroku's release: command maps one-to-one onto deploy hooks, so your migration-on-deploy workflow survives intact.
Heroku vs. Deploynix: An Honest Comparison
Before the runbook, a fair assessment of both platforms. We build Deploynix, so discount our bias accordingly, but we've tried to be straight about where Heroku genuinely wins.
Heroku
Strengths: The best-polished PaaS developer experience in the industry, fifteen years of refinement, and truly zero server operations. You never think about the OS, patching, or capacity in units smaller than a dyno. The addon marketplace makes bolting on a service a one-line command. The buildpack system means deployment "just works" for almost any stack with no configuration at all.
Best for: Prototypes, internal tools, and early-stage products where engineering time is worth far more than infrastructure spend. Teams with zero appetite for operations, even automated operations. Apps whose traffic genuinely justifies elastic dyno scaling on unpredictable schedules.
Considerations: The cost curve steepens sharply with scale, and you pay the managed-platform premium on every component: compute, database, cache, logging, scheduling. The 512MB memory ceiling on Standard-1X dynos is tight for a modern Laravel app with Octane or heavy queue jobs. The ephemeral filesystem forces architectural decisions on you. Fine-grained data residency requires Private Spaces pricing.
Deploynix
Strengths: You run on servers you own, at VPS prices, with the operational work automated. Provisioning on DigitalOcean, Vultr, Linode, Hetzner, AWS, or any custom VPS takes minutes. Zero-downtime deploys, instant rollback, deploy hooks, queue workers and daemons via Supervisor, the Laravel scheduler, free SSL, monitoring with alerts, and automated database backups to S3-compatible storage are all built in. Security hardening is applied by default at provision time.
Best for: Production Laravel applications with steady traffic, teams that want a 60-90% infrastructure cost reduction without giving up push-to-deploy workflows, and anyone who needs to choose exactly where their data lives.
Considerations: A VPS is your responsibility in ways a dyno is not. Deploynix automates OS hardening, unattended security upgrades, backups, and monitoring, but you are still the one choosing when to scale the server, and vertical scaling means a resize rather than a slider. If your traffic is genuinely spiky by 10x on unpredictable schedules, elastic dyno scaling still has a real argument. We compare that trade-off across providers in our guide on migrating Laravel from Railway, Render, or Fly.io to a VPS.
The Pre-Migration Audit: What to Inventory Before You Move
A clean migration starts with a complete inventory, and in our experience the audit takes under an hour for a typical app. Teams that skip it are the ones that discover a forgotten Heroku Scheduler job or an addon-injected config var two days after cutover. Work through these four checks before you provision anything.
1. Buildpacks and Procfile
List what Heroku is actually running for you:
heroku buildpacks -a your-app
cat ProcfileA typical Laravel app shows the PHP buildpack (often with Node.js for asset builds) and a Procfile like this:
web: heroku-php-nginx -C nginx.conf public/
worker: php artisan queue:work redis --tries=3 --timeout=90
release: php artisan migrate --forceEach line becomes a row in your migration plan: web: becomes the Nginx site Deploynix configures automatically, worker: becomes a queue worker defined in the UI, and release: becomes a deploy hook. If you have custom buildpacks (wkhtmltopdf, ImageMagick, custom PHP extensions), note them; on a VPS these are one-time apt installs rather than buildpack gymnastics.
2. Addon Inventory
heroku addons -a your-appFor each addon, decide: self-host it, replace it, or keep it as an external SaaS. Postgres and the Key-Value Store move onto your server. Logging addons are replaced by real log files on disk plus Laravel's own channels. Scheduler is replaced by cron. Error trackers like Sentry or Bugsnag are ordinary SaaS products that work identically from a VPS; keep them and just carry the DSN over.
3. Export Your Config Vars
heroku config -a your-app --shell > heroku-config.envThis produces KEY=value lines ready to merge into your new .env. Watch for two gotchas. First, addon-injected vars like DATABASE_URL and REDIS_URL will change; don't copy them blindly. Second, Heroku's PHP buildpack reads DATABASE_URL as a single URL, while a conventional Laravel .env splits it into DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, and DB_PASSWORD. Translate accordingly, or keep using DATABASE_URL since Laravel supports it natively.
4. The Database Decision (Spoiler: There Isn't One)
Heroku means PostgreSQL. Some VPS panels are MySQL-only, which turns a hosting migration into a database engine migration, an order of magnitude more risk. Deploynix provisions PostgreSQL as a first-class option alongside MySQL and MariaDB, so you keep your engine, your extensions, and your query behavior. The only decision left is which Postgres major version to install; match your Heroku version, which you can check with heroku pg:info -a your-app.
The Migration Runbook, Step by Step
The full migration takes an afternoon of focused work plus a parallel-running window, and no step involves downtime until the final DNS cutover. We'll assume a standard Laravel app with Postgres, Redis-backed queues, and scheduled jobs. Here's the sequence we run ourselves.
Step 1: Provision the Server
In Deploynix, connect your cloud provider (DigitalOcean, Vultr, Linode, Hetzner, AWS) or add a custom VPS by IP, then provision. For the worked example later in this post, a 2 vCPU / 4GB server is comfortable. Choose PostgreSQL as the database engine during provisioning; Valkey is installed for cache and queues. Provisioning applies security hardening automatically: firewall rules, SSH lockdown, fail2ban, and unattended security upgrades.
Step 2: Move the Database
Capture a fresh backup on Heroku, download it, and restore it to the new server:
heroku pg:backups:capture -a your-app
heroku pg:backups:download -a your-appThis gives you latest.dump in Postgres custom format. Restore it:
pg_restore --verbose --clean --no-acl --no-owner \
-h your-server-ip -U deploy -d your_app_db latest.dumpThe --no-acl --no-owner flags matter: Heroku's dump references Heroku-internal roles that don't exist on your server, and these flags strip them. For a database of a few gigabytes this completes in minutes. This first restore is a rehearsal; you'll do it once more, fresh, right before cutover.
Step 3: Create the Site and Connect GitHub
Add a site on the server, point it at your GitHub repository (GitLab and Bitbucket work the same way), and enable auto-deploy on push to your production branch. Set your environment variables from the audited heroku-config.env, with the database and Redis vars updated:
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=your_app_db
DB_USERNAME=deploy
DB_PASSWORD=generated-at-provision
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=redisThen recreate your release phase as deploy hooks, which run on every deployment:
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cacheDeploys are zero-downtime: each push builds a new release directory and switches a symlink only when everything succeeds, so a failed build never takes the site down. Trigger a first deploy and confirm the app boots against the restored database.
Step 4: Queue Workers and the Scheduler
Recreate each worker: Procfile line as a queue worker in the Deploynix UI, which writes and manages the Supervisor configuration for you:
Command: queue:work
Connection: redis
Tries: 3
Timeout: 90
Processes: 2Supervisor restarts workers if they crash and Deploynix restarts them gracefully on each deploy, replacing the dyno restart behavior you relied on. For Heroku Scheduler jobs, the fix is nicer than what you had: Heroku Scheduler's 10-minute minimum granularity goes away. Enable the Laravel scheduler and every schedule() definition in your app runs on its native cadence via a single cron entry:
* * * * * php /home/deploy/your-app/current/artisan schedule:run >> /dev/null 2>&1If any Scheduler jobs lived only in the Heroku dashboard rather than in code, move them into routes/console.php now. Your scheduling belongs in version control anyway.
Step 5: The Parallel-Running Window
Don't cut over the day you finish setup. Run both environments in parallel for three to seven days. Point a test subdomain at the new server, enable the free SSL certificate, and exercise the app: logins, uploads, queue jobs, scheduled tasks, webhooks. Watch the built-in monitoring for memory and CPU headroom. Keep deploying to both targets during this window so the codebases never diverge.
One rule during parallel running: the Heroku database remains the source of truth. The new server's database is a stale copy used for verification only. Any writes made during testing on the new stack are throwaway.
Step 6: DNS Cutover With a Low TTL
At least 24 hours before cutover, drop your DNS TTL to 300 seconds so the switch propagates in minutes rather than hours. At cutover time:
heroku maintenance:on -a your-app
heroku pg:backups:capture -a your-app
heroku pg:backups:download -a your-app
pg_restore --verbose --clean --no-acl --no-owner \
-h your-server-ip -U deploy -d your_app_db latest.dumpThen flip your DNS A record to the new server, confirm SSL issues for the apex domain, and verify propagation:
dig +short yourapp.comTotal write-downtime is the maintenance window: the duration of one final dump and restore, typically 5-15 minutes for a moderately sized database. Read-only traffic could even be served throughout if you skip maintenance mode and accept a small window of writes to reconcile.
The Rollback Plan
Keep the Heroku app scaled up but idle for two weeks. If anything goes seriously wrong, rollback is the cutover in reverse: dump the new database, restore to Heroku, turn maintenance off, flip DNS back. Because your TTL is still 300 seconds, the world follows within minutes. Only after two quiet weeks do you run the satisfying final commands:
heroku ps:scale web=0 worker=0 -a your-app
heroku addons:destroy heroku-postgresql -a your-app --confirm your-appWhat Does the Move Actually Save? A Worked Example
For a typical production Laravel app, the same workload that costs about $115/month on Heroku runs on a $12-24/month VPS, a saving of 79-90% on infrastructure. That's consistent with the 30-60% average that repatriating organizations report (MassiveGRID, 2026); Heroku migrations land above the average because every component carries the managed premium. Here are the numbers, using Heroku's pricing at the time of writing:
Component | Heroku | VPS (Deploynix-managed) |
|---|---|---|
Web serving | Standard-1X dyno, 512MB RAM: $25/mo | Included on server |
Queue worker | Standard-1X worker dyno: $25/mo | Supervisor worker, included |
PostgreSQL | Standard-0 addon: ~$50/mo | Self-hosted, included |
Redis / cache | Key-Value Store Premium-0: ~$15/mo | Valkey, included |
Scheduled jobs | Scheduler addon: free, 10-min granularity | Cron, per-minute, included |
SSL | Included | Included (Let's Encrypt) |
Total compute | ~$115/mo | $12-24/mo (e.g. Hetzner 4GB ~$12, DigitalOcean 4GB ~$24) |
And the hardware comparison is lopsided in the VPS's favor. That $115 Heroku stack gives your app two 512MB containers and a shared database plan. The $12 Hetzner server gives you 4GB of RAM and 2 dedicated-share vCPUs for the app, database, and cache combined, with room to spare. Most apps migrate onto a smaller server than they expect.
Two honest caveats. Add your Deploynix subscription to the right-hand column; even so, the total stays a fraction of the Heroku bill, and the platform fee is flat rather than scaling per-dyno. And if your app needs Performance dynos ($250+ each at the time of writing), your savings are far larger than this example, which is exactly the dynamic we walk through in cloud repatriation for Laravel: from AWS to a $10 VPS. For the small end of the spectrum, the real cost math on a $5 VPS breaks down the same comparison at hobby scale.
What Do You Take On When You Leave Heroku?
Leaving a fully managed platform means accepting responsibility for three things Heroku handled invisibly, and the honest answer is that automation covers most, not all, of them. Here's the real operational delta, item by item, and how much of it is mitigated.
OS Updates and Security
On Heroku, the stack image is patched for you. On a VPS, the OS is yours. Deploynix narrows this gap substantially: provisioning applies hardening by default (firewall, SSH configuration, fail2ban) and enables unattended security upgrades, so CVE patches land without your involvement. What remains yours is the occasional major distribution upgrade every couple of years. That's a real task, but it's a scheduled afternoon, not a lurking daily burden.
Backups
Heroku Postgres continuous protection was genuinely good, and this is the responsibility to take most seriously. The mitigation: automated database backups to any S3-compatible storage (Backblaze B2, Wasabi, Cloudflare R2, DigitalOcean Spaces) on a schedule you define, with retention policies. Configure this on day one, before cutover, and restore-test it once. We wrote a full setup guide in automated database backups: set it, forget it, sleep well. Off-server backups cost a dollar or two per month; skipping them is the one genuinely reckless way to run this stack.
Monitoring
heroku ps and the metrics dashboard are replaced by built-in server monitoring: CPU, memory, and disk tracking with configurable alerts, plus visibility into your Supervisor-managed workers. You'll actually gain observability here, because you can see the whole machine rather than a per-dyno abstraction, and read full logs on disk instead of Heroku's 1,500-line router buffer. Pair it with the external error tracker you already carried over, and your coverage exceeds what you had.
Frequently Asked Questions
Do I have to switch from PostgreSQL to MySQL?
No. This is the question we hear most from Heroku teams, and the answer is a flat no. Deploynix provisions PostgreSQL as a first-class engine alongside MySQL and MariaDB. Your pg_dump restores directly with pg_restore, your migrations are untouched, and any Postgres-specific features you use (JSONB queries, full-text search) keep working.
How much downtime should I expect?
The write-downtime equals your final dump-and-restore window, typically 5-15 minutes for databases up to a few gigabytes, and the rest of the migration happens while Heroku serves production normally. With a 300-second DNS TTL set in advance, traffic follows the cutover within minutes. Larger databases can shrink the window further with logical replication, but most apps don't need it.
What happens to my Heroku Scheduler jobs?
They move to the Laravel scheduler, and they get better. Heroku Scheduler's minimum granularity is 10 minutes and its jobs live outside version control. On your server, a single cron entry runs schedule:run every minute, so every frequency Laravel supports works natively and your schedule definitions live in routes/console.php where they belong.
Can I still roll back a bad deploy like heroku releases:rollback?
Yes, and it's faster. Every zero-downtime deploy keeps previous releases on disk, and rollback is a one-click symlink switch back to the prior release, taking effect in seconds. Failed builds never go live in the first place, because the symlink only moves after a successful build.
Is this migration harder than moving from Forge?
Slightly, because you're changing hosting models rather than just panels, but the Laravel-side work is nearly identical. If you've read our Forge migration guide, the site, worker, and scheduler steps will look familiar; the Heroku-specific additions are the Procfile translation and the Postgres dump-and-restore, both covered above.
The Next Step
Heroku was the right choice when your priority was shipping fast with zero operations, and there's no shame in having paid the premium while it bought you focus. But when the invoice crosses the line from convenience fee to burden, the math is hard to argue with: the same Laravel workload, on hardware four times larger, for 79-90% less, with PostgreSQL intact and your deploy workflow preserved.
The whole migration is an afternoon of setup plus a week of parallel running, and nothing is irreversible until you scale the dynos to zero. The single next step: provision a server with Deploynix on the provider of your choice, restore last night's pg_dump to it, and point a test subdomain at the result. Once you've watched your app run for a week on a $12 server, the invoice decision makes itself.