Skip to main content

Buildpacks vs the Self-Hosted PaaS Pack: Why Coolify, Dokploy, and Kubero All Default to Heroku's git-push Instead of Cloud Native Buildpacks

9 min readDora NodaDora Noda
Share

Heroku invented the buildpack in 2011, then spent 2018 helping build its successor — Cloud Native Buildpacks, a CNCF-hosted, vendor-neutral standard for turning source code into an OCI image without a Dockerfile. Eight years later, the self-hosted tools recreating Heroku's git push experience — Coolify, Dokploy, Kubero — still don't default to it. Two of them ship real, spec-compliant support for it anyway. None of them lead with it. Here's what each one actually runs when you push code, and why the open standard lost the default slot on infrastructure its own inventor helped design.

What each tool actually runs on git push

ToolDefault builderReal CNB support?How the build actually runsProduces an OCI image?
CoolifyNixpacksNoRuns Railway's Nixpacks binary, which inspects the repo and emits a generated Dockerfile, then does a standard docker build on it. Falls back to a repo-root Dockerfile if present.Yes
DokployNixpacksYes, selectableSame Nixpacks-generates-a-Dockerfile path by default — but the build-type picker also offers Dockerfile, Railpack, and genuine Buildpacks using the pack CLI against either the heroku/builder (Heroku Buildpacks, version-pinned, default v24) or a Paketo builder image.Yes, all four paths
KuberoDockerfile / NixpacksYes, selectableOffers Dockerfile and Nixpacks like the others, plus direct buildpacks.io support — and its own "runpacks," a fetch→build→run pattern that skips image-building entirely.Yes for Dockerfile/Nixpacks/CNB; no for runpacks
Railway (origin of Nixpacks)Railpack (beta, March 2026)NoBuilt Nixpacks in 2022 as a Nix-based alternative to CNB, then outgrew it — Railpack drops the Nix dependency entirely for a purpose-built Go analyzer, cutting image size 38% for Node and 77% for Python versus Nixpacks.Yes

Two things in that table cut against the easy version of this story. Dokploy doesn't have "its own buildpack-style detection" in the sense of having rolled a competing standard — it ships the actual CNB lifecycle via the pack CLI, pointed at either Heroku's own builder image or Paketo's, as one of four selectable build types. Kubero doesn't have "a template system" as its build layer either — its templates are pre-configured service catalog entries (WordPress, Redis, Postgres), a separate feature from its build layer, which includes real buildpacks.io support alongside Dockerfile and Nixpacks. Coolify is the only one of the three with zero CNB support anywhere in its build-type list.

So the accurate claim isn't "these tools reinvented buildpacks instead of adopting the standard." It's narrower and more interesting: none of them made CNB the default, even the two that ship it as a fully-supported option. That's a deliberate choice, not an oversight, and it's worth understanding why.

The CNB contract vs. the one-binary alternative

Cloud Native Buildpacks define a multi-phase lifecycle: a detect phase where each buildpack in an ordered group checks the source tree and returns whether it applies, a build phase where the matched buildpacks contribute layers, and an export phase that assembles the final image. That lifecycle doesn't run against your Dockerfile-less repo directly — it runs inside a builder image (which bundles the lifecycle binary, the buildpacks, and a build-time base) and produces output layered onto a separate, slimmer run image. Executing it locally means installing the pack CLI or calling a platform API; executing it in CI means pulling the builder image, running the lifecycle, and pushing the result — a registry round-trip even for an image staying on the same machine.

Nixpacks and Railpack collapse that into one step: a single binary walks the repo, decides what's there (a package.json with a build script, a requirements.txt, a go.mod), and emits a Dockerfile — or in Railpack's case, builds directly via BuildKit — that a normal docker build turns into an image. No builder/run image pair to version, no separate CLI to install, no registry hop required for a local build. For a platform that already owns the box the image will run on, that's strictly less infrastructure to stand up for the same git-push-to-running-container outcome.

That difference compounds on maintenance, not just on the first build. A CNB-based operator has to track and periodically rebuild against upstream builder/run image updates — Paketo ships new stack images on its own cadence, and a stale builder means CVEs sitting in a base layer nobody's rebuilt. Dokploy's own Buildpacks option pins a specific heroku/builder version (defaulting to 24) precisely because that pairing has to be tracked explicitly, not assumed. A single-binary builder pushes that same "which base image versions am I on" question down into the generated Dockerfile, where it's one file to read instead of a builder-image dependency graph to reason about.

Why "just reuse the standard" is harder than it sounds

CNB's actual value proposition is portability: a spec-compliant image built by any CNB-conformant tool runs on any CNB-conformant platform, because the builder/run-image split and the OCI output are the interchange format. That matters enormously when the entity building the image and the entity running it are different — which is exactly Heroku's and Pivotal's original situation, and why GCP's Buildpacks-based Cloud Run and Cloud Foundry both adopted the same spec. You build once, and the image is legible to any platform that speaks the contract.

A self-hosted PaaS that provisions its own nodes, owns the Cluster API layer, and runs its own container runtime doesn't need that portability guarantee nearly as much — it already controls both ends. What it needs is the fastest, most legible path from a git push to a running container on infrastructure it already fully owns, and every extra moving part (a builder image to keep patched, a pack CLI to invoke, a registry round-trip) is overhead against that goal with no portability payoff to justify it. That's the calculus Dokploy and Kubero are actually making: they keep real CNB support in the menu — it's a credible answer for a team migrating off Heroku or Cloud Foundry that already has CNB-shaped buildpacks — but they don't hand it the default slot, because the default needs to optimize for their own runtime, not for a runtime they don't operate.

Coolify skipped the CNB option altogether rather than pay to maintain it, and picked up Nixpacks — someone else's non-CNB, single-binary tool — as a ready-made default instead of building or maintaining its own from scratch.

The more radical answer: don't build an image at all

Kubero's "runpacks" is the outlier worth naming on its own, because it doesn't just skip CNB — it skips the OCI-image-build step that Nixpacks, Railpack, and CNB all still produce. A runpack is three containers chained as fetch → build → run: the fetch stage pulls the repo, the build stage runs the language-specific build commands against a well-maintained public base image, and the run stage serves the result — without ever assembling and pushing a new image to a registry. It trades the reproducibility and portability of "here is the exact image that ran" for the latency of skipping the build-push-pull round trip entirely. That's a legitimate axis this whole comparison sits on: CNB optimizes for portability, Nixpacks/Railpack optimize for a fast one-command image build, and runpacks optimizes past both of them for raw deploy latency by not producing an artifact at all.

What a Cluster-API-based platform should bet on

For bex — a Cluster-API-based PaaS provisioning owned Hetzner nodes rather than reselling someone else's runtime — the Nixpacks/Railpack model is the better default, not CNB's builder/run-image lifecycle. The reasoning holds for the same reason it holds for Dokploy and Kubero: bex already owns the containerd runtime the image lands on, so CNB's portability guarantee buys nothing a same-platform build doesn't already have, while its builder-image-plus-pack-CLI machinery is exactly the kind of extra moving part that a single-binary detect-and-build step avoids.

There's a second reason that's specific to a platform built for AI agents as first-class operators: a build that produces one artifact — an image digest — from one log stream is something an agent can reason about and retry deterministically. A four-phase lifecycle spread across a builder image, a run image, and an external CLI is a much larger surface for an agent to model correctly when a build fails and it has to decide what to retry. Simpler default, same reason Nixpacks and Railpack won the default slot everywhere else: fewer parts to explain when something breaks.

That doesn't mean shutting the door on CNB entirely. Keeping a Dockerfile escape hatch is non-negotiable — some builds need control that CNB and Nixpacks both abstract away — and genuine CNB support, positioned the way Dokploy positions it, as an explicit path for teams importing existing Heroku or Cloud Foundry buildpacks rather than the primary engine, is a reasonable answer to "we're migrating off Heroku and already have buildpacks" without making everyone else pay for infrastructure they don't need.

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.

Sources

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