Skip to main content

Heroku Died but Its Buildpacks Run Everywhere: heroku/builder:26 on Cloud Run and the Builder Contract a Self-Hosted PaaS Owes

9 min readDora NodaDora Noda
Share
On this page

The most durable thing Heroku ever shipped doesn't run on Heroku. It runs on your laptop, on Cloud Run, on a Hetzner box you own — anywhere an OCI image runs. It's the builder: heroku/builder:26, the Cloud Native Buildpack builder image that turns a git repo with a Procfile into a running container with no Dockerfile involved, maintained in the open after the platform it was named for went into sustaining-engineering mode.

Here is the whole proof, up front, in three files and two commands. A project.toml that pins the builder:

toml
schema-version = "0.2"
 
[io.buildpacks]
builder = "heroku/builder:26"

A Procfile like the one your app already has:

procfile
web: gunicorn app:app --bind 0.0.0.0:$PORT
worker: celery -A app.celery worker

And one command that builds it into a plain OCI image with Heroku nowhere in the runtime path:

bash
pack build my-app --builder heroku/builder:26 --path .
docker run --rm -e PORT=8080 -p 8080:8080 my-app

That image deploys to Cloud Run with gcloud run deploy --image, and Google's own docs describe the tighter seam: gcloud run deploy --source . builds from source with buildpacks, and a project.toml in the repo sets which builder it uses. Build with Heroku's contract, run on Google's serverless, pay neither vendor for the build layer. The rest of this post is the receipts for each half of that sentence — and what it obligates any self-hosted PaaS that wants ex-Heroku apps to land on it unchanged.

What builder:26 actually is (and why it isn't builder:24)

Heroku's Cloud Native Buildpack builders are versioned against Ubuntu LTS releases, and the number after the colon is the stack generation. heroku/builder:24 sits on Ubuntu 24.04 and is the default for every CNB build on the Heroku platform today. heroku/builder:26 is the next generation: its builder image was opened as an experimental build on Ubuntu 26.04 — "Resolute Raccoon," released April 23, 2026 — in heroku/cnb-builder-images PR #935, explicitly so Heroku's language teams could begin CNB preparation ahead of the LTS.

That "experimental" origin matters less than what followed. Heroku's Dev Center now documents both builders as supported: CNB builds default to heroku/builder:24, and quote, "Heroku currently only supports the heroku/builder:24 and heroku/builder:26 builders," with the override living in the app repo itself via the builder field in project.toml. Apps on the CNB stack report their stack value as cnb — the stack is the contract now, not a set of Ubuntu packages on a dyno.

Three things distinguish the :26 line from the :24 line this site already covered in the Fir migration playbook. First, it tracks the newest LTS base image, which is where runtime currency (new OpenSSL, new system libraries) arrives first. Second, every Heroku language buildpack README — Node, JVM, .NET, Ruby, Python — now documents heroku/builder:26 as its example builder, which is how you can tell where Heroku's own buildpack teams do their daily work.

Third, and most important for this post: the builder is a public image you pull, not a platform feature you rent. Nothing in the :26 workflow requires a Heroku account, a Heroku pipeline, or a dyno. The platform is exit-only. The builder is infrastructure.


The Cloud Run proof: Google's docs describe this exact seam

Skepticism is warranted here — "runs everywhere" is doing a lot of work in the title. So here is the narrow, checkable version of the claim, in two layers.

The airtight layer: pack build emits a standard OCI image. That is not marketing; it is the output contract of the Cloud Native Buildpacks spec, and every Heroku buildpack README ends the same way — build with --builder heroku/builder:26, then docker run it. An OCI image built that way deploys to Cloud Run with --image exactly like an image built from a Dockerfile. No Heroku component participates at deploy time or runtime. The buildpack's job ended when the image was exported.

The documented layer is tighter: Cloud Run deploys from source with buildpacks built in. gcloud run deploy --source . builds container images from source "without having to install Docker on your machine or set up buildpacks," per Google's deploy-from-source docs. And Google's buildpacks documentation has a page literally titled "Use a specific builder": a project.toml in the repo sets the builder for a source deploy.

Google's default is its own gcr.io/buildpacks/builder, and Google's own comparison of buildpacks vs. Jib vs. Dockerfile names Heroku and Paketo builders as the alternatives that slot into the same --builder flag. The seam is vendor-neutral by design, not by accident.

Read those two facts together and the split the title promises becomes concrete. The build contract (detect the language, compile the app, honor the Procfile, emit an image) and the runtime contract (serve HTTPS on $PORT, scale to zero, roll back) are separable purchases now. Heroku used to sell them bundled. Google will sell you the runtime while Heroku's open-source builder does the build. A self-hosted fleet is simply the third buyer at the same counter: same builder, own machines, no per-seat platform toll.


Anatomy of the builder contract: four pieces, each with a failure mode

"Support the Heroku builder" is vague enough to implement wrong. The contract an ex-Heroku app actually depends on has four pieces, and each one has a visible failure mode when a platform honors the others but skips it:

Contract pieceWhat it isWhat breaks when it's ignored
The builder image (heroku/builder:24 / :26)The pinned build+run image pair: language detection, compilation, and the Ubuntu-based run image the app executes onApps build against one base and run on another; system-library drift (OpenSSL, glibc) surfaces as runtime crashes, not build errors
The Procfile → process types (heroku/procfile CNB)The Procfile buildpack reads web: / worker: entries and registers them as image process types, with web defaultMulti-process apps lose their formation: the worker never starts, or the platform invents its own entrypoint and the Procfile becomes decorative
The $PORT conventionThe web process binds to the port the platform injects, not a hardcoded oneHealth checks fail on an app that runs fine locally — the classic "works with docker run, dead on the platform" support ticket
Default process selectionWith no Procfile, language buildpacks register a default web process (and evolve it — the Ruby buildpack moved defaults to IPv6 :: binding in 2026)Builds fail outright at export: the CNB lifecycle errors with "tried to set web to default but it doesn't exist" when no process type was defined — a real failure filed upstream, not a hypothetical

The table is also a migration estimator. If a platform honors all four rows, an ex-Heroku repo deploys with zero repo changes: same builder, same Procfile, same $PORT handling, same default. Every missing row is a repo edit the migrating team has to make — a rewritten Procfile, a hardcoded port replaced, a Dockerfile written from scratch — and repo edits are where migrations stall, because each one needs the original author's context to get right.


What a self-hosted PaaS owes: the checklist, and why you must own the builder

For a git-push platform deciding its own build layer — bex included — the Heroku lesson compresses into a checklist and one strategic decision.

The checklist, derived from the table above:

  1. Accept heroku/* builders by reference. If the app pins builder = "heroku/builder:26" in project.toml, build with it. Don't translate it to your own builder silently; silent translation is how "builds on my machine" becomes "fails on the platform" with no diff to inspect.
  2. Honor Procfile process types as the formation. web: and worker: in the repo must become running processes without a platform-side manifest rewrite. Any alternative that replaces the Procfile with a proprietary manifest is a migration tax levied on every arriving app.
  3. Inject $PORT and route to it. The smallest row in the table generates the most support tickets when skipped.
  4. Own builder versioning: track new builders, rebase old images, keep base images current. When :28 arrives on the next Ubuntu LTS, your users' rebuilds should pick up patched base images without repo changes — the rebase behavior Paketo already demonstrates and this site's CVE-rebasing coverage documented.

Item 4 is where the checklist becomes the strategic decision: own the builder; don't rent each vendor's opaque build. Two precedents from this site's own 2026 coverage show both failure modes. Railway rented, then swapped, then swapped again: Heroku Buildpacks and Paketo out in February 2025 for Nixpacks, then Nixpacks out in March 2026 for the in-house Railpack — two builder migrations in thirteen months, each one re-deciding image sizes and language coverage for every user at once. (Railpack's claimed wins: ~38% smaller Node and ~77% smaller Python images.)

Dokploy chose plurality instead — Nixpacks, Railpack, Heroku Buildpacks, and Paketo side by side — and bought four configuration surfaces, four failure modes, and a recommended default (Railpack) that can't build Ruby, Java, or Rust next to a broad default (Nixpacks) capped at Node 23 with no active upstream.

Both are the same mistake from opposite sides: the builder is somebody else's product decision, so the platform inherits somebody else's roadmap churn. Owning the builder means the heroku/* contract is a compatibility surface you implement and version — pinned builders, reproducible builds, base-image rebases on your schedule — rather than a dependency you forward to whichever upstream moved last. The CNB spec makes that ownership practical in a way the old v2 buildpack API never did: builders are images, process types are metadata, and pack build runs the same build locally that the platform runs remotely. There is no privileged platform-side build step left to rent.


The split is the point

Heroku the platform is in sustaining mode: no new features, no new Enterprise contracts, maintenance only. Heroku the build contract — versioned builders, the Procfile-to-process-types mapping, the $PORT convention, all of it foundation-governed and multi-vendor maintained — keeps shipping: a :26 builder tracking the newest Ubuntu LTS, and per Heroku's own March 2026 disclosure, 27 CNB releases in 14 months with contributors from Heroku, Google, Red Hat, and VMware.

That divergence is the whole argument. A build contract that outlives its vendor is infrastructure in the same sense as OCI itself: nobody asks which company's idea a container image was. The teams that kept their heroku/builder:26 references while deleting their Heroku pipelines understood this before the retrospectives did. And the bar for every platform courting them — hosted or self-hosted — is now mechanical, not rhetorical: take the repo as it is, builder pin and Procfile included, and produce a running service. Everything else is commentary.

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.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex