Skip to main content

Kamal Supports Cloud Native Buildpacks, Not Just Dockerfiles: What That Means for bex's Own Build Path

8 min readDora NodaDora Noda
Share
On this page

Kamal exists to strip PaaS abstraction back down to SSH and Docker commands. No dashboard, no control-plane daemon, no managed anything — just a YAML file and kamal deploy. So it's a genuinely odd fact that, since version 2.7.0, Kamal ships a builder option for Cloud Native Buildpacks (CNB): the same source-to-image technology that powers Heroku-style "no Dockerfile" deploys on hosted PaaS platforms. The tool built to remove abstraction just added one of PaaS's core abstractions back in, as an option.

What Kamal's pack Builder Actually Does

This isn't a vague partnership announcement — it's a config block. PR #916, merged into Kamal 2.7.0 in June 2025, added a pack builder alongside Kamal's default dockerfile builder. In deploy.yml, it looks like this:

yaml
builder:
  pack:
    builder: heroku/builder:24
    buildpacks:
      - heroku/ruby
      - heroku/procfile

builder: heroku/builder:24 names the buildpack builder image — the base that carries the detection and build logic, comparable to picking a language runtime tier on a hosted PaaS. buildpacks: lists which buildpacks run, in order — here, Heroku's Ruby buildpack followed by its Procfile buildpack, which reads a Procfile to figure out how to start the app, the same convention Heroku popularized over a decade ago. Under the hood, Kamal shells out to the pack CLI (the CNB project's own build tool) instead of docker build, and the result is a normal OCI-compliant image that the rest of Kamal's deploy pipeline — SSH, kamal-proxy, health checks — treats exactly like an image built from a Dockerfile. Teams that want finer control can drop a project.toml next to their code, which pack picks up automatically for things like custom buildpack ordering or environment variables baked into the build.

That speed difference isn't incidental — it's the whole point of buildpacks as a build strategy. A CNB build runs three phases: detect (each buildpack in the list inspects the source tree and declares whether it applies — heroku/ruby looks for a Gemfile, heroku/procfile looks for a Procfile), build (each detected buildpack contributes layers — installed gems, compiled assets, a language runtime — independently of the others), and export (the layers assemble into a final OCI image). Because each buildpack's output is its own cache-addressable layer, a rebuild that only changed application code can skip re-fetching gems or reinstalling a runtime entirely, the same layer-caching benefit Docker gives you if you write your Dockerfile carefully — except here it's the buildpack author's job to get the caching right, not each individual project's.

The origin is worth noting: this wasn't a 37signals-initiated feature. Developer Nick Hammond opened a GitHub discussion in May 2024 proposing buildpacks as a Kamal builder option, reporting a Rails app that built in 1:35 cold and 14 seconds on a cached rebuild — the detect/build/export cache paying off exactly as designed. Buildpack maintainer Richard Schneems (Heroku) weighed in on which buildpack implementations were production-ready, noting Paketo as a more modular alternative to Heroku's own set. Hammond then found that Paketo's image-labels buildpack solved a real integration snag: Kamal identifies which containers it manages by reading image labels, and a stock buildpack build doesn't attach those the way a hand-written Dockerfile's LABEL instruction would. He prototyped a fix using Kamal's pre-connect hook and a project.toml, then turned the working setup into PR #916. It shipped because a community member did the integration work end to end, not because 37signals decided Dockerfiles needed a competitor.

Before PR #916, a Kamal project with no Dockerfile simply had no build path — Kamal never shipped its own auto-detection the way Coolify and Dokploy did from early on. That's a meaningfully different starting point than "which zero-config builder should we pick." Kamal wasn't choosing CNB over building something in-house; it was choosing CNB over continuing to have nothing at all for developers who didn't want to hand-write a Dockerfile. Reaching for an existing CNCF spec instead of writing a bespoke detector from scratch, even to fill a real gap, is itself the signal worth reading.

Dockerfile Stays the Default — This Is an Opt-In

Nothing about this changes what a fresh Kamal project does out of the box: write a Dockerfile, or don't deploy. The pack builder sits next to dockerfile as an alternative you opt into per project, not a replacement. That framing matters, because it's a genuinely different choice than the one other self-hosted PaaS tools made when they hit the same "some users don't want to write a Dockerfile" problem:

ToolZero-config build optionWhere it came from
Kamalpack builder (opt-in; Dockerfile stays default)Reuses the CNCF-governed Cloud Native Buildpacks spec via the pack CLI
CoolifyNixpacks (until v4.1 added Railpack as a second beta option)Depends on Railway's in-house builder(s)
DokployBuilt-in buildpack-style auto-detectionBespoke detection logic maintained in Dokploy's own codebase
bexCloud Native Buildpacks (default build path)Same CNCF-governed spec Kamal consumes as an option

Coolify and Dokploy both wrote (or adopted a single vendor's) zero-config detection layer specifically to spare users from writing a Dockerfile. Kamal — the tool with the strongest institutional allergy to reinventing infrastructure — had the same problem and chose the opposite move: consume an existing, multi-vendor-governed spec instead of writing a third bespoke builder. Cloud Native Buildpacks joined the CNCF as an Apache-2.0 project in 2018 and moved from sandbox to incubation in 2020; Heroku, Google, VMware/Pivotal, and the Paketo project all maintain buildpacks against the same spec, so no single company can retire it out from under adopters the way Railway retired Nixpacks in favor of Railpack earlier this year. For a minimal tool like Kamal, that governance property is exactly what makes bolting on an optional zero-config path safe — there's no vendor roadmap risk to inherit from something you only offer as a choice, not a default.

What It Means for bex's Own Build Path

The honest question the title poses is direct: should bex follow Kamal's split — Dockerfile as the default, Cloud Native Buildpacks as an opt-in — instead of the reverse? bex's build path already defaults new repos to Cloud Native Buildpacks, with a Dockerfile as the documented escape hatch for anything that doesn't fit the convention. Kamal's decision doesn't argue for flipping that. It argues for keeping it.

The difference is who's on the other end of the config file. Kamal's users are, by definition, comfortable enough with Docker to hand-write a Dockerfile — that's the tool's entire audience, the reason "no abstraction" is the pitch that sells it. Defaulting them to Dockerfile and offering buildpacks as an option for the subset who'd rather not costs that audience nothing; it's additive, not a migration. bex's tenants are a git push, expecting a running HTTPS service on the other end, the same expectation Heroku, Render, and Railway trained an entire generation of developers to have. Forcing a Dockerfile as the day-one default for that audience reintroduces the exact friction a git-push PaaS exists to remove — it turns "push code, get a URL" into "first, learn to write a multi-stage Dockerfile correctly enough that layer caching doesn't silently break your build times."

That's not a hypothetical cost. A Dockerfile written by someone who isn't a build-pipeline specialist routinely gets caching wrong — a COPY . . placed before RUN npm install invalidates the dependency layer on every single code change, turning what should be a ten-second incremental rebuild into a multi-minute one. A buildpack's detect/build/export split doesn't let a tenant make that mistake, because the tenant never writes the layer boundaries — the buildpack author already got them right, once, for everyone downstream. That's exactly the caching benefit Hammond's 1:35-cold/14-second-rebuild numbers demonstrate, and it compounds differently depending on the audience: it's a nice-to-have for someone who already writes correct Dockerfiles, and it's the difference between a fast platform and a slow one for someone who doesn't.

So buildpacks-first is the right default for a platform whose users didn't sign up to write infrastructure code, and Dockerfile-first is the right default for a tool whose users signed up specifically because they wanted to. Both defaults are internally consistent with who each tool is for — the same technology, opposite ordering, for opposite reasons.

What Kamal's pack builder actually validates is the spec choice underneath both defaults, not the ordering. A tool with zero tolerance for unnecessary abstraction looked at "how do we let users skip writing a Dockerfile" and reached for Cloud Native Buildpacks — the same CNCF-governed spec bex already treats as its default build path — rather than writing a fourth bespoke auto-detector to sit alongside Nixpacks, Railpack, and Dokploy's own. That's a strong endorsement, even if the ordering doesn't transfer: the safest bet for a build layer's core technology is still the one no single vendor can retire on a roadmap decision, whether it's the fallback option in a minimal SSH tool or the load-bearing default in a Cluster API-based platform.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, built on Cluster API and Cloud Native Buildpacks by default. Star the repo on GitHub or deploy your first app today.

Sources

All figures and quotes above are drawn directly from the linked 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