Skip to main content

The Heroku Exit Playbook's Missing Half: Pipelines and Review Apps on Machines You Own

11 min readDora NodaDora Noda
Share
On this page

The Heroku exit playbook moved your app, your add-ons, and your data off the platform — and then, in its most honest paragraph, admitted the part it left behind: per-PR review apps are "not automatic anywhere outside Heroku," so budget time to stand one up by hand. That paragraph understated the gap by half. Teams leaving Heroku never just encoded infrastructure in the platform. They encoded release process: Heroku Pipelines carrying code from review to staging to production, and Review Apps spinning up a disposable copy of the app — add-ons included — for every pull request.

This post is the CI/CD sequel. Here is the entire mapping up front, so you can stop reading after the table if you only came for the answer:

Heroku surfaceWhat it really doesOwned-fleet equivalent
Pipeline promotion (Promote to production)Copies staging's running slug to production as a new release — no rebuildKargo Freight promotion across Stages, or GitHub Actions environments with protection rules
pipeline:diffShows which commits staging is ahead bygit log between deployed SHAs, or Kargo's Freight lineage in the dashboard
Review Apps (auto-create per PR)Builds a disposable app per pull request from app.jsonPer-PR namespace + app manifest, created by a PR webhook
Review-app add-onsProvisions partner-optimized (or cheapest) add-on plans automaticallyExplicit dependency declarations per preview, or shared backing services
Wait-for-CI + destroy-staleGates creation on CI green, destroys idle apps after N daysThe same two rules in your PR automation, with a TTL you pick
postdeploy / pr-predestroy scriptsRuns setup on create, cleanup on destroyDeploy/destroy hooks in the preview lifecycle
Automatic review-app PostgresGives every PR its own databaseCopy-on-write DB branch, schema-per-PR, full clone, or seed-only — see the cost table below

The rest of this post earns each row: what the Heroku behavior actually guarantees, what breaks if your replacement drops the guarantee, and what the database row honestly costs — because that last row is where every "we'll just do preview envs" plan meets its real bill.

Pipelines, concretely: the slug is the artifact​

A Heroku Pipeline looks like a deployment convenience — a "Promote to production" button on the staging app. The semantic underneath is stricter than the button suggests: promotion copies the already-built slug from the upstream app to the downstream app as a new release. Nothing recompiles. The byte sequence that passed staging is the byte sequence that serves production. Rebuilding for production would invalidate the staging signal, and Heroku's design refuses to let you do it by accident.

That no-rebuild invariant is the row that matters most in the table above, because it is the one most hand-rolled replacements silently drop. A CI pipeline that builds once, pushes an image, and then promotes the same image digest through environments preserves it. A pipeline that re-runs docker build per environment — even from the same commit — does not: dependency resolution, base-image drift, and timestamped assets mean the artifact staging tested is not quite the artifact production runs. If your owned-fleet promotion flow rebuilds, you kept the button and lost the guarantee.

Heroku's own sharp edge is worth carrying over as a checklist item rather than a surprise. Because promotion copies the slug verbatim, anything baked in at staging build time travels to production unchanged — including frontend assets compiled with staging API URLs or staging credentials. Teams hit this the first time a "production" deploy serves a page calling the staging API. The fix is the same on any platform: build-time variables must be environment-agnostic, with per-environment values injected at release time, not compile time. Promotion makes this a hard rule instead of a suggestion, because there is no rebuild step left to paper over the difference.

For the promotion engine itself, two options cover nearly every team:

Kargo, when promotion is a process. Kargo — the CNCF Sandbox project from the Argo creators at Akuity — models delivery as Warehouses producing immutable Freight (versioned bundles of artifact references) that move through Stages (dev, staging, production) with verification steps and approval gates. It complements Argo CD rather than replacing it: Argo syncs declared state into the cluster, Kargo decides which artifact version each environment should want next. Pick it when you have more than one service sharing a release train, when production promotion needs a human approval with an audit trail, or when "who promoted what, when" must be answerable after the fact. The cost is a real control plane to run: Kargo's CRDs, its dashboard, and the discipline of defining Stages per environment.

Plain GitHub Actions environments, when promotion is a button. For one app with a review-to-staging-to-production flow, GitHub's environments with protection rules (required reviewers, wait timers, branch restrictions) reproduce the pipeline shape with no extra infrastructure: staging deploys on merge to main, production deploys the same artifact on manual approval. You lose Kargo's freight lineage and cross-service orchestration; you gain zero new components. Most teams migrating a single Heroku pipeline should start here and graduate to Kargo only when the release process outgrows one repo's workflow file.

Either way, pipeline:diff has a trivial replacement: the deployed SHAs per environment, diffed with git log. The habit worth keeping is checking it before promoting, not the command that prints it.

Review Apps, concretely: app.json is the spec​

Review Apps feel like magic — open a PR, get a URL — but the machinery is a manifest plus three lifecycle rules, and each maps cleanly:

The manifest. app.json declares everything a fresh copy of the app needs: environment variables, add-ons, buildpacks, and setup scripts. Your owned-fleet equivalent is whatever manifest already describes your app — a Helm values file, a Kustomize overlay, a bex.yml — parameterized by PR number. The porting work is mechanical: every app.json key has an obvious counterpart, and the translation is a good forcing function for finding the settings your team set by clicking in the dashboard three years ago and never wrote down.

The add-ons. Here Heroku does something genuinely hard to replicate: when app.json names an add-on, the platform provisions a plan the add-on partner optimized for review workloads — or the cheapest plan when no review-specific plan exists. Nobody on your team picks a Postgres tier per PR; the platform picks the disposable one.

On your own fleet, this row splits two ways. Dependencies that are cheap to share (object storage buckets with PR-prefixed paths, a shared Redis with namespaced keys) become shared backing services with per-PR isolation by naming convention. Dependencies that need real isolation become per-preview instances of the smallest shape you run — which is a capacity-planning decision Heroku used to make for you, and now lives in your preview manifest as explicit resource requests.

The lifecycle rules. Three checkboxes in the pipeline settings carry the whole operational load: create a review app automatically for new PRs, wait for CI to pass before creating it, and destroy stale apps automatically after a chosen idle period — plus automatic destruction when the PR merges or closes. Reproduce all three, not just the first. The CI gate exists because building previews for red PRs burns compute and trains reviewers to ignore preview links.

The stale-destroy rule exists because review apps bill like regular apps, and a PR left open for three weeks with a running dyno is how the "free" preview workflow shows up on the invoice. On your own fleet the currency is node capacity rather than dyno-hours, but abandoned namespaces accumulate exactly the same way — a TTL with a visible countdown beats a quarterly cleanup ticket.

The hooks. postdeploy runs setup after the first release succeeds (migrations, seed scripts); pr-predestroy runs cleanup before destruction (deregistering the preview's custom domain is the canonical example). These map directly to deploy and destroy hooks in whatever drives your preview lifecycle. The pr-predestroy side is the one teams forget: every external registration a preview creates — DNS record, OAuth callback URL, third-party webhook — needs a deregistration path, or your offboarding story for previews is a graveyard of stale integrations.

The database is the honest bill​

Every row above is automation you can write in a week. This row is architecture, and it is where "we'll just do preview envs" meets the cost Heroku used to absorb: every review app gets its own provisioned database, automatically, with production-shaped data available through the seed path. Four options exist on an owned fleet, and the honest version of this section prices all of them:

OptionProvision timeFidelityReal cost
Copy-on-write DB branchSecondsFull production data, isolatedA storage layer that supports thin clones (Neon-style branching, ZFS snapshots, or your cloud volume's clone API) plus automation to branch on PR open and drop on close
Schema-per-PR on a shared instanceSecondsProduction schema, synthetic or subset dataNear-zero infra cost, but migrations must be schema-scoped and one noisy PR can pressure the shared instance
Full clone / restore per PRMinutes to tens of minutesFull production data, isolatedWall-clock time on every PR plus storage per preview — fine for small databases, prohibitive past tens of gigabytes
Seed-only empty databaseSecondsSchema plus fixtures onlyCheapest and fastest, but reviewers test against fiction — data-dependent bugs sail through

There is no option that is instant, faithful, and free, which is exactly why Heroku's automatic provisioning felt like magic: the platform ate the tradeoff and billed you dyno-hours. For most teams the right answer is copy-on-write branching where the storage layer allows it — Neon demonstrated the pattern with instant Postgres branches wired to Vercel previews, seed data inherited from the parent — falling back to schema-per-PR for workloads where shared-instance pressure is acceptable, and reserving seed-only for PRs that genuinely don't touch data behavior. What doesn't work is picking seed-only by default and discovering six months later that every data-migration bug now reaches staging. The database decision is the one preview-environment choice that compounds, so make it deliberately and revisit it when the database doubles.

The freeze stopped all three at once — migrate in this order​

The forcing function is dated: on February 6, 2026, Salesforce moved Heroku into sustaining engineering — stability, security, and reliability work only, no new features, no new Enterprise contracts, with product investment redirected toward Salesforce's Agentforce AI products. The freeze matters for this post's scope because it stopped all three surfaces together. The app runtime, the Pipelines promotion flow, and Review Apps are now fixed in amber as a set: none of them will gain the capability your migration wishes they had, and the longer the release process lives on the frozen platform, the more of your team's shipping muscle memory depends on behavior nobody maintains.

Migrate in this order, and each step de-risks the next:

  1. Promotion flow first. It has the fewest moving parts (same artifact, more environments) and the highest blast radius if rushed — get the no-rebuild invariant running on Kargo or Actions environments while Heroku still hosts production, and promote a few quiet releases through the new path before it carries anything urgent.
  2. Preview lifecycle second. Port app.json to your manifest, wire the three lifecycle rules, and run both systems in parallel for a few PRs so reviewers can compare URLs before the Heroku ones go away.
  3. Data strategy last. It is the only step that touches production data shapes, so it earns the caution of going last — and by the time you reach it, the preview lifecycle driving the branch-or-schema decision already exists to plug into.

None of these steps requires waiting for Heroku to force your hand with a breaking change nobody budgeted time for. The platform in sustaining mode doesn't break on the day of the announcement; it breaks quietly, months later, when the workflow your team depends on needs something the frozen platform will never ship. The exit playbook moved the app. This half moves how the app ships.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with preview environments and promotion-shaped workflows that survive leaving a sustaining-mode platform behind. Star the repo on GitHub or read the Heroku migration guide before you start your own cutover.

Related articles

Check your move before you migrate

Free browser tools: check a render.yaml or your Render scripts against bex, or turn a Heroku app or docker-compose.yml into a draft render.yaml. Nothing you paste leaves your browser.

Open the migration tools