Your Heroku Dockerfile has an expiration date, and it is not printed on the dyno. Heroku's Fir generation — generally available since April 2025 and the only Heroku with a future — cannot deploy Docker images. The Container Registry stays Cedar-only and amd64-only, heroku.yml does not work on Fir, and since February 2026 the whole platform sits in "sustaining engineering": no new features, no new Enterprise contracts, maintenance only. If your app deploys as a container, the platform's roadmap has quietly written you out of it.
So here is the answer up front, with every receipt below. For one typical stack — two web dynos, one worker, a small Postgres, a small Redis — the bill today is $87/month on Heroku, ~$35–60/month across Render, Railway, and Fly.io where your Dockerfile runs unchanged, and ~$7/month on a self-hosted fleet you own. The migration itself is mostly mechanical: your existing amd64 image runs as-is on all three hosted exits. The full breakdown, the destination matrix, and the step-by-step playbook follow.
Three disclosures before the receipts. First, these are list-price recomputes from public rate cards and Dev Center docs, not scanned invoices — every rate below names its plan so you can redo the math against the public rate cards. Second, the Heroku leg uses the same $87 reference stack as this site's earlier line-by-line Heroku bill so the two posts stay comparable; the data-service tier is the small-prod one, not the cheapest hobby rung. Third, ops time is excluded from all sides, which flatters self-hosting more than the managed options — the "what to do Monday morning" section prices that back in honestly.
What exactly breaks: the Cedar–Fir gap for Docker teams
Heroku's generations page and language-support policy state the situation plainly: the Container Registry and Runtime feature deploys custom Docker images to Cedar-generation apps only, and deploying Docker images is not supported on Fir-generation apps. The registry itself is Cedar-only and amd64-only.
On Fir, builds go exclusively through Cloud Native Buildpacks via the heroku/builder:24 builder. Classic buildpacks do not work there, and building Docker images with heroku.yml and the container stack is unsupported.
The rest of the gap list reads like a checklist of things Docker teams tend to rely on:
| Capability | Cedar | Fir |
|---|---|---|
Docker image deploys (heroku.yml, container stack) | Yes | No |
| Container Registry arch | amd64-only | Not available on Fir at all |
| Classic buildpacks | Yes | No — Cloud Native Buildpacks only |
| CPU architecture | x86-64 | ARM-only (AWS Graviton) |
| Monorepo deploys | Supported patterns exist | Unsupported |
| Heroku CI | Yes | Unavailable for Fir pipelines |
| Shield (compliance) spaces | Yes | On the roadmap |
Two nuances keep this honest. First, Heroku backported Cloud Native Buildpack support to Cedar-generation apps in September 2026, so you can modernize your build on Cedar without touching Fir — but that changes how slugs are built, not where Docker images run. The registry stays Cedar-only either way.
Second, Fir is genuinely good technology for the teams it serves: Graviton dynos, blue/green deploys, native OpenTelemetry, more than 500 dynos per space. The problem is not that Fir is bad. The problem is that a Docker-based team cannot follow it there, and Cedar is now a platform in maintenance mode. "Stay on Cedar" is a tenancy on a frozen floor, not a plan.
The three hosted exits where your Dockerfile runs unchanged
The good news buried in the gap table: every major Heroku alternative accepts the artifact Heroku is abandoning. Your existing amd64 image — the same one Cedar runs today — deploys on all three without a rebuild, because all three run x86-64 fleets. The ARM question only appears if you later choose ARM nodes for your own fleet, covered in the playbook.
| Destination | Dockerfile support | Deploy motion | Entry price signal |
|---|---|---|---|
| Render | Native Docker runtime from git or registry | render.yaml blueprint or dashboard | ~$7/mo per Starter service |
| Railway | Dockerfile auto-detected from repo | Connect GitHub, push | $5/mo base plus usage |
| Fly.io | Docker-native (fly launch, fly deploy) | CLI plus fly.toml | Pay-per-use from ~$5/mo |
None of these asks you to rewrite around a buildpack. Render's Docker support covers both git-backed builds and prebuilt images; Railway detects a Dockerfile at the repo root and builds it; Fly.io is a Dockerfile-first platform where the container is the unit of deployment.
For a team whose deploy artifact is already an image, these are all smaller conceptual jumps than rewriting the app to satisfy Fir's buildpack-only pipeline — and unlike a Fir migration, each one preserves the exact runtime your CI already tests.
The honest differentiators are not about Docker at all: Render sells simplicity and managed data services, Railway sells per-second usage billing with no per-service tax, and Fly.io sells global anycast regions at the cost of a more hands-on networking model. Pick on those axes. On the Dockerfile axis, all three say yes.
Real bills: the same stack, five ways
The reference stack is the smallest thing a real side-project SaaS runs in production: two web instances for zero-downtime deploys, one worker for background jobs, a small Postgres, and a small Redis. Priced at September 2026 list rates:
| Line item | Heroku (before) | Render | Railway | Fly.io | Self-hosted Hetzner (after) |
|---|---|---|---|---|---|
| 2x web compute | 2x Standard-1X at $25 (512 MB each) = $50 | 2x Starter at $7 = $14 | Usage share | 2x shared-cpu 1x ≈ $10 | Included in node |
| 1x worker compute | 1x Standard-1X = $25 | 1x Starter = $7 | Usage share | 1x shared ≈ $5 | Included in node |
| Small Postgres | Essential-1 = $9 | Basic tier ≈ $6–20 | Usage ≈ $10–20 | Small volume/self-managed ≈ $10 | Co-located, $0 |
| Small Redis | Mini tier = $3 | Key-value ≈ $10 | Usage ≈ $5–10 | Upstash/small ≈ $10 | Co-located, $0 |
| Base/subscription | $0 | $0 | $5 base | $0 (~$5 spend typical) | 1x cloud node ≈ $7 |
| Total | $87/mo | ~$40–50/mo | ~$45–60/mo | ~$35–45/mo | ~$7/mo |
The Heroku column is the documented $87: Standard-1X dynos at $25 with 512 MB each, Essential-1 Postgres at $9, the mini Redis tier at $3. The destination columns are ranges, not quotes, because usage billing (Railway) and tier boundaries (Render Postgres, Fly volumes) move with your actual consumption — but the shape is stable across reasonable assumptions: roughly half the Heroku bill on any hosted exit, roughly one-twelfth self-hosted.
The sensitivity note is the most useful row in this table, and it is not a row: the data services drive the delta, not the compute. Three Starter services on Render cost $21 against $75 of dynos — a clean win, but only 3.5x. The multiplier opens up the moment managed Postgres and Redis enter, because Heroku prices its smallest serious database tiers like standalone products while every destination either bundles smaller data tiers cheaply or lets you co-locate. Any migration estimate that prices compute and waves at "plus database" as negligible is doing it backwards: price Postgres and Redis first, and the compute comparison will take care of itself.
One more variable that looks scary and is not: architecture. Your Cedar-era images are amd64, and every hosted exit above runs amd64 fleets, so nothing needs rebuilding on the way in. Rebuilds enter only if you self-host on ARM nodes (Hetzner's CAX line, AWS Graviton equivalents) — a one-line docker buildx change to produce a multi-arch image, done once, tested in CI like any other matrix entry.
The self-hosted playbook: buildpack-plus-Dockerfile on your own fleet
If the ~12x multiplier cleared your hurdle rate, here is the end-to-end path. It preserves both deployment motions Heroku teams know — git push builds via buildpacks for the simple services, Dockerfiles for the ones that already have them — on machines you own.
1. Containerize what buildpacks currently build. Anything still deploying via classic buildpack needs a Dockerfile before it can move anywhere, including to Fir. Start with the official language image matching your runtime, copy the buildpack's behavior (asset compilation, migration hooks in the release phase), and run the result locally with the same env file production uses. This step is the same work a Fir migration would force on you — do it once, benefit everywhere.
2. Make images multi-arch in CI. Add a linux/arm64 target to your existing linux/amd64 build so the same pipeline serves hosted exits and ARM self-hosted nodes alike. Push both to a registry you control (GitHub Container Registry suffices). Pin digests in deploy manifests, not floating tags.
3. Stand up the fleet and its routing. One modest cloud node runs the whole reference stack; add a second when you want the database separated from the app tier. Each service gets a declarative app resource (image, env, replicas, health checks), a shared ingress terminates TLS per custom domain, and DNS cutover happens per service, not flag-day.
4. Move the data with a freeze window. Postgres moves via pg_dump/pg_restore with writes paused — or logical replication for near-zero downtime if the business justifies the setup. Match extensions and collations to the target version before you start; that mismatch is the classic failed-cutover story. Redis moves by enabling AOF/RDB persistence on the new instance and seeding from a snapshot; reconfigure eviction policy to match what the managed tier enforced silently.
5. Rewire the Heroku-isms. This is the real checklist, and each item is small: scheduler jobs move to cron or a job runner; log drains become a logging vendor account plus a forwarder; review apps per pull request become preview environments per pull request (same idea, different control plane); add-on-provided monitoring becomes an agent plus a dashboard you own. None of it is hard; all of it is the work the $87 was quietly paying for.
6. Cut over per service, keep Cedar warm. Point DNS at the new ingress one service at a time, watch error rates and P95 latency per cutover, and keep the Cedar app scaled (even to one dyno) for a week as instant rollback. Delete nothing until the first full billing cycle closes on the new side.
What to do Monday morning
The decision rule fits in three branches. If your app has no Dockerfile and no plans for one, Fir's constraints do not bind you: evaluate Fir on its merits (Graviton pricing, blue/green deploys, OTel), and treat Cedar's maintenance mode as a slow-burn deadline, not a fire. If you ship containers and want to stay managed, take a hosted exit — your image runs unchanged, the bill roughly halves, and the migration in the playbook above shrinks to steps 1 (already done), 4, and 5. If the bill still stings after halving, self-host: the same containers on flat-priced hardware keep the git push workflow while deleting the per-dyno meter, at the price of owning backups, restores, and the 3 AM page.
Whichever branch you take, do the one thing that is free: this week, inventory which of your services deploy via Dockerfile, heroku.yml, or the Container Registry. That list is the exact set of workloads with no future on Heroku's only forward platform — and the size of that list is the size of your migration, whether you schedule it or it schedules you.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



