Every git-push PaaS eventually has to answer the same question: when a developer pushes a repo with no Dockerfile, how does the platform figure out what to build? Write the detection logic yourself — a script that greps for package.json, checks for a requirements.txt, guesses at a Ruby version from a Gemfile.lock — or adopt Cloud Native Buildpacks (CNB), the CNCF-hosted spec that Heroku's original buildpacks evolved into and that Paketo, Google Cloud Buildpacks, and Cloud Foundry all build on today.
It looks like a small implementation detail. It isn't. One path buys years of community-maintained language coverage; the other quietly turns "detect and build a container" into a permanent staffing line for the platform team.
The real cost isn't build speed — it's a decade of Dockerfiles nobody wants to touch
The engineering team at Doximity ran into this the hard way, and their public writeup on buildpacks versus Dockerfiles is one of the more honest accounts of what the choice actually costs in practice. Their problem wasn't a single slow build. It was maintenance debt spread across their entire fleet.
When a shared dependency needed a security patch, the Dockerfile-based workflow meant opening "pull requests to a dozen repositories to update a dozen Dockerfiles written a dozen different ways" — because every team had hand-rolled its own Dockerfile, and no two looked alike. Some cached poorly. Some pinned the wrong base image. Some nobody remembered how to safely edit anymore.
With buildpacks, the same fix was a version bump. Doximity's team "bumped the buildpack or stack to a newer version and rolled out the change" — one change, applied uniformly, across every app that used the buildpack. The optimization knowledge that used to live in a scattered pile of hand-tuned Dockerfiles — when to invalidate a cache layer, how to keep dependency layers separate from application-code layers — moved into the buildpack implementation itself. As Doximity put it, when a Gemfile.lock changes, the Ruby buildpack already knows it needs to rebuild the dependency layer "in a way that keeps the gems that didn't change" cached. No developer has to know that trick. No platform engineer has to re-teach it to the next new hire.
That's the deliverable this post is actually about: not a synthetic build-speed number, but the maintenance curve a platform team is signing up for. A hand-rolled build script is not a one-time cost — it's an ongoing one, and it compounds with every language, every framework version, and every base-image CVE the team now owns forever.
Reproducibility and SBOMs are the spec's job, not a script's
Two more things fall out of standardizing on CNB that a bespoke detect-and-build script has to build from scratch, if it bothers to build them at all.
Reproducible builds. Cloud Native Buildpacks' own documentation states the guarantee plainly: "given the same inputs, two builds should produce the same outputs." Concretely, running pack build repeatedly against the same source, builder image, and buildpack set produces images with the same image ID locally, and the same registry digest when published remotely. The mechanism is unglamorous — the lifecycle zeroes out layer timestamps, which is why a freshly built CNB image can show a creation date decades in the past — but the guarantee is real and documented, not a marketing claim.
It comes with an honest limitation worth keeping: the CNB lifecycle "cannot fix non-reproducible buildpack layer file contents," so full reproducibility still depends on the underlying language ecosystem behaving deterministically too (a pip install that resolves differently run to run will still break the guarantee). A bespoke build script gets none of this by default — reproducibility becomes something the platform team has to design and test for, on top of everything else, rather than something the build tooling gives it for free.
Software Bills of Materials. CNB buildpacks can populate SBOM data — a full list of the software components baked into an image — in CycloneDX, Syft, and SPDX formats, retrievable with a single pack sbom download command. That's a real head start over stitching a separate image-scanning step onto a bespoke pipeline after the fact.
The ecosystem is leaning further into this, not away from it. On July 21, 2026, BellSoft announced a hardened Paketo builder image built on its Alpaquita OS, committing to a "zero-CVE window" — a patched image published within 24 hours of any vulnerability disclosure — with "each published image" carrying "a full Software Bill of Materials and a verifiable provenance record." BellSoft also reported buildpack tool adoption across its customer base more than doubled year-over-year from 2024 to 2025. Whatever a platform team would build in-house to keep base images patched and audit-ready, someone upstream is now selling as a maintained product — which is exactly the kind of work a bespoke pipeline forces a team to keep doing itself, indefinitely.
Where the convention actually breaks
None of this means Dockerfiles are obsolete, and CNB's own documentation doesn't pretend otherwise. The detect phase works by pattern-matching a repo against the file structures each buildpack knows how to recognize — a package.json here, a pom.xml there. When an app's dependency shape doesn't match any of those conventions, detection simply fails, and the project's own guidance is candid about it: for applications a buildpack can't auto-detect, "the more common and practical solution for most teams is to use a Dockerfile."
In practice, that gap shows up in a few recognizable shapes:
- Multi-language monorepos, where a single repo mixes a Python service, a Node frontend, and a Go worker in ways no single buildpack's detect script is designed to disambiguate.
- Apps needing non-standard system-level dependencies — a specific
aptpackage, a custom-compiled native library — that sit outside what a language-specific buildpack provisions. - Unconventional directory layouts, where the files a detect script looks for exist but aren't where convention expects them.
None of these are exotic edge cases dreamed up to hedge the argument — they're common enough that every serious CNB-based platform keeps Dockerfile support as a first-class, sanctioned path rather than an apologetic fallback. Heroku's own 2026 preview of Cloud Native Buildpacks for local machines ports the classic buildpack-detection experience to a developer's laptop via the standard pack build CLI, but Heroku has never required every app to be buildpack-detectable — Dockerfile support has stayed alongside it. Google Cloud Buildpacks and Cloud Foundry's Paketo integration follow the same pattern: convention by default, Dockerfile by explicit choice, and nobody treats reaching for the escape hatch as a failure.
There's a middle option worth naming, too: writing a custom buildpack rather than falling all the way back to a Dockerfile. That's the right call when a platform genuinely expects to see the same unusual dependency shape repeatedly — an internal framework a company runs across dozens of services, say — because the maintenance cost of one well-tested buildpack, reused everywhere, beats both a pile of near-identical Dockerfiles and a detect script trying to special-case one framework among many. It's the wrong call for a one-off repo with a genuinely bespoke build; that's what the Dockerfile escape hatch exists for. The mistake to avoid is treating "we can't auto-detect this" as a signal to start writing general-purpose detection logic instead of reaching for the tool built for exactly that case.
The decision that actually matters for a platform team
Put together, the shape of the tradeoff is less "buildpacks versus Dockerfiles" and more "what does a platform team want to spend its engineering hours maintaining."
| Hand-rolled detect-and-build script | CNB convention (Paketo, Google Cloud Buildpacks, etc.) | |
|---|---|---|
| New language/framework support | Platform team writes and tests it | Upstream buildpack maintainers ship it |
| Base-image CVE patching | Platform team's ongoing responsibility | Upstream-maintained (e.g., BellSoft's 24-hour patch window) |
| Build-cache correctness per language | Reinvented per script, inconsistently | Encoded once, per buildpack, by people who know that ecosystem |
| Reproducible image digests | Not guaranteed unless explicitly engineered | Documented guarantee, with known caveats |
| SBOM / provenance | Bolted on as a separate scanning step | Native output, per buildpack, in standard formats |
| Apps that don't fit any convention | Whatever the script's author anticipated | Explicit Dockerfile fallback, by design |
The honest reading of that table is that a platform team shouldn't be trying to out-engineer Paketo's Ruby buildpack maintainers on cache-layer ordering, and it shouldn't be trying to reinvent SBOM tooling that BellSoft already ships as a maintained product. What it should build — carefully, and as its own real feature rather than an afterthought — is the escape hatch: reliable Dockerfile detection and support for the repos that convention genuinely can't reach, so "no Dockerfile found" never becomes a dead end for a developer who just wants their app running.
That's the same bet bex is built on: git-push deploys detect a Dockerfile when one exists, and fall back to a Render-compatible buildpack path when it doesn't — without asking a team to hand-maintain either half. The platform's job is to make that switch invisible to the developer, not to compete with a decade of upstream buildpack maintenance.
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:
- Buildpacks vs Dockerfiles — Doximity Technology
- Reproducibility — Cloud Native Buildpacks docs
- SBOM downloads — Cloud Native Buildpacks docs
- BellSoft ships hardened, zero-CVE builder images for Paketo Buildpacks — Cloud Native Now
- Dockerfile vs Buildpacks: Which Should You Choose? — Miget
- Detect — Cloud Native Buildpacks docs



