Migrating Off Heroku: What It Actually Costs and What to Do First

We've run a lot of Heroku migrations in the last two years. A startup wants to move to EKS or GKE, they know they need to migrate eventually, and either the bill has gotten uncomfortable or a customer's security questionnaire has asked who has access to their production environment. The conversation always starts the same way: "How hard is it, really?"

The honest answer: it depends entirely on how much of your infrastructure is Heroku-native. If you've been using Heroku Postgres, Heroku Redis, Heroku Scheduler, Heroku Connect, and Heroku's review app feature — each of those is a separate workstream. If you're using Heroku mostly as a deployment platform and your databases are already on RDS or somewhere else, a migration to a comparable platform like Render, Railway, or Fly.io can take a week. A full migration to Kubernetes for a team with no K8s experience takes 6–10 weeks minimum, and that's if everything goes well.

Here's what we actually do when we run one of these.

The first question: where are you actually going?

Most teams come to us knowing they want to "get off Heroku" but without a clear picture of what they want to move to. The options are meaningfully different:

  • Another PaaS (Render, Railway, Fly.io): Lowest migration effort. If your app is 12-factor and you're not deeply embedded in Heroku-specific add-ons, you can often be live in days. Similar pricing model, similar operational experience. The tradeoff: you're still abstracted from infrastructure, which is fine for many teams and a problem for teams that need fine-grained control.
  • ECS or App Runner (AWS): A step up in control, a step up in complexity. Good fit for teams already in the AWS ecosystem who want container orchestration without full Kubernetes. The operational surface is smaller than EKS and the migration path for a Heroku app is relatively clean.
  • EKS / GKE / AKS: Full Kubernetes. Maximum control, maximum operational complexity. The right choice if you have 20+ services, need fine-grained resource control, are building toward a platform team, or have workloads that genuinely require Kubernetes primitives (custom scheduling, daemonsets, stateful workloads). The wrong choice for a team of 5 engineers whose main concern is that Heroku got expensive.

We've talked multiple teams out of going straight to Kubernetes because the operational overhead wasn't justified by their scale. The Heroku bill was $3,000/month; the engineering cost of maintaining a Kubernetes cluster would be higher. Render at $1,200/month with a two-day migration was the right answer.

What the migration actually looks like

Phase 1: Audit what you have (days 1–3)

Before touching anything, we inventory every Heroku resource and its equivalent on the target platform:

  • Every dyno type (web, worker, scheduler) and its resource consumption pattern
  • Every Heroku add-on — database, cache, logging, monitoring, email, search — and the target replacement
  • Every environment variable (these become Kubernetes Secrets or cloud-native secrets manager entries)
  • The deployment process — how code gets to Heroku today and what CI/CD change is needed
  • Custom domain configuration and SSL certificates
  • Any Heroku Review Apps usage (if present, this is the hardest piece to replicate)

The audit surfaces the surprises. In one recent engagement, we found that a "simple" Rails app was using Heroku Scheduler for 11 different cron jobs, three of which had no documentation and one of which nobody remembered creating. Understand what you have before you start moving it.

Phase 2: Containerize the application (days 3–7)

If the team hasn't already Dockerized their application, this is usually the most time-consuming part. Heroku builds from a Git push and handles the runtime environment through buildpacks — no Dockerfile required. Moving off Heroku requires a Dockerfile.

This isn't usually technically hard, but it catches assumptions. Buildpacks handle a lot of things automatically that a Dockerfile requires explicitly — system dependencies, runtime version pinning, asset compilation, the order of operations during build. Writing a good Dockerfile for a Rails or Node app that has only ever run on Heroku takes 1–3 days and testing time, not 30 minutes.

What we write: a multi-stage Dockerfile (separate build and runtime stages to keep the production image small), a .dockerignore that excludes test files and dev dependencies, and a local docker-compose.yml for development. The compose file is important — it's the first time most teams have a reproducible local environment that matches production.

Phase 3: Set up the target environment (days 5–14)

This runs in parallel with containerization. For a Kubernetes migration, this is where we spend the most time:

  • Cluster provisioning via Terraform (EKS, GKE, or AKS depending on the cloud)
  • Ingress controller (Traefik or NGINX), cert-manager for TLS, ExternalDNS for automatic DNS records
  • Secrets management setup — AWS Secrets Manager or Vault, with a controller that injects secrets as environment variables at pod startup (matching Heroku's model, which developers are used to)
  • Monitoring and logging — Prometheus + Grafana or DataDog, centralized log aggregation
  • CI/CD pipeline — replacing the Heroku Git deploy with a proper pipeline that builds the Docker image, runs tests, and deploys to the cluster

Phase 4: Database migration (days 10–20)

This is the most risk-laden part and the one we plan most carefully. If the team was on Heroku Postgres, they need to move to RDS, Cloud SQL, or a managed Postgres on their target platform. Options:

  • Pg_dump and restore: Simple, works for databases under 50GB with acceptable downtime windows. We schedule a maintenance window, stop writes, dump from Heroku, restore to the new host, update connection strings, restart.
  • Logical replication: For larger databases or zero-downtime requirements. Set up a replica on the target from the Heroku source, let it sync, then do a cutover with a brief write pause. Requires Postgres 10+ and is not always possible depending on Heroku Postgres plan and extensions.
  • AWS DMS or equivalent: For cross-engine migrations (e.g., Heroku Postgres to Aurora). More configuration, more testing required, but handles large datasets with minimal downtime.

Phase 5: Cutover (days 18–25)

We do a full rehearsal first — a complete cutover to the new environment in a staging configuration, using a copy of production data. The rehearsal almost always finds something: a missing environment variable, an S3 bucket permissions issue, a health check endpoint that doesn't exist at the expected path.

The actual production cutover:

  1. Lower DNS TTL to 60 seconds, 24 hours before cutover
  2. Deploy the final version to the new environment
  3. Database cutover (maintenance window if using dump/restore; near-zero pause with logical replication)
  4. Update DNS to point to the new ingress IP
  5. Monitor for 30 minutes — error rates, latency, database connections, application logs
  6. Keep Heroku running for 48 hours as rollback option

What teams underestimate

The technical work is known and repeatable. What teams consistently underestimate:

  • Developer workflow changes: Heroku's git push heroku main is elegant. Developers who are used to it need to adapt to a new mental model — PRs trigger CI, CI builds images, ArgoCD deploys. The new model is better for production, but it's unfamiliar and there's a week of adjustment.
  • The review apps problem: Heroku Review Apps (ephemeral environments per PR) is genuinely difficult to replicate on Kubernetes without significant investment. Teams that rely on it heavily need to either build a similar system (expensive) or change how they do QA (disruptive). Plan for this early.
  • Ops knowledge gap: After the migration, the team owns a Kubernetes cluster. That's a new responsibility. Someone needs to know how to investigate a failing pod, how to handle a node that goes NotReady, how to interpret Prometheus alerts. If there's no DevOps capacity in-house, this is the argument for keeping an outsourced team engaged post-migration rather than treating the migration as a handoff.

The cost picture

The financial case for moving off Heroku to self-managed Kubernetes is real, but it requires honesty about the full cost. Infrastructure costs drop — a Heroku Standard-2 dyno at $50/month compares to a Kubernetes pod that costs $8–15/month on EKS once you factor in shared node costs. For a team with 20+ dynos, the savings are material.

But the operational cost is real. A managed Kubernetes cluster (EKS, GKE) costs $150–200/month in management fees before a single node. The engineer time to maintain the cluster is ongoing. For a team with 5 engineers who've never touched Kubernetes, the true cost of ownership is higher than the infrastructure cost suggests. For a team that's ready for that investment or has DevOps support, the economics work.

"We'd been dreading the Heroku migration for two years. It took four weeks and we wish we'd done it sooner. The local environment alone was worth it."

Planning a Heroku migration?

We'll audit your current Heroku setup and give you a realistic migration plan — which platform to target, what the timeline looks like, and what the true cost of ownership will be.

Book Free Migration Assessment
← Back to all articles