Skip to main content

Nixpacks vs Buildpacks vs Dockerfile in 2026: The Cache-Hit Numbers Behind Railway and Render's Builder Bets

11 min readDora NodaDora Noda
Share
On this page

Railway spent three years and 14 million app builds learning that its zero-config builder was taxing every deploy — then rewrote the builder from scratch. Render, facing the same physics, stayed the course on buildpacks. Both vendors are answering one question: when a git-push PaaS builds the same app hundreds of times, which build abstraction wastes the least work on rebuilds 2 through 200?

That question has a concrete answer, and it fits in one table. Everything below is the explanation of why the table looks the way it does.

The three-way numbers, up front

No single lab has ever built identical apps through all three builders on identical hardware, so treat this as a compiled comparison: each cell comes from a published measurement, cited in the section it belongs to. The apps are the two most representative PaaS workloads — a standard Next.js app and a standard Python app.

BuilderCold build (Next.js)Warm rebuild, code-only changeImage size (Next.js)Cache behavior on repeat deploys
Nixpacks (zero-config, Nix)~87sNear-cold: env injection invalidates layers on nearly every deploy~1.3 GBPoor: single Nix-store layer, no build/runtime split
Cloud Native Buildpacks (Paketo)Slowest single build (detect phase + layer export)Seconds; base-image CVE patch via pack rebase in under 1s~319 MB (Spring Boot reference)Best: content-addressed layers reused independently
Hand-written multi-stage Dockerfile~15s~12s cached (code-only change reuses dependency layers)~77 MB (Next.js standalone)Good if ordered well: sequential invalidation, one bad line order ruins it
Railpack (Railway's Go + BuildKit successor)~42s (independent operator, same app: 76s Nixpacks)~37s warm~152 MB (same-app: 337 MB Nixpacks)Good: BuildKit layer granularity, cache shared across environments

Two patterns jump out before any prose. First, cold-build speed and warm-rebuild speed crown different winners: the Dockerfile dominates the first build, while buildpacks dominate every patch thereafter. Second, Nixpacks loses on all four columns at once — which is exactly why Railway stopped developing it.

Why each builder caches differently

Every gap in that table traces to one architectural decision per builder: what the unit of cache reuse is.

Nixpacks caches at the granularity of a Nix store dump. Nixpacks resolves dependencies through Nix and ships everything — compilers, headers, build tooling, runtime libraries — in a single undifferentiated /nix/store layer. There is no mechanism to split "what the build needed" from "what the running app needs," so a code-only change still ships the toolchain, and Railway's habit of injecting deployment-specific variables into the build invalidated cache layers on nearly every deploy. The determinism Nix is famous for survives (same inputs, same store), but determinism is reproducibility, not reuse: knowing a build is reproducible does not make it incremental.

Cloud Native Buildpacks cache at the granularity of content-addressed layers. A buildpack-built image is a stack of independently reusable layers — OS layer, runtime layer, app layer — each keyed by content hash. Change application code and only the app layer rebuilds; patch a base-image CVE and pack rebase swaps the OS layer underneath the untouched app layers in under a second, with no rebuild at all. The price is paid on cold builds: the detect phase walks every registered buildpack to figure out what the app even is, and layer export adds overhead a Dockerfile never pays.

A Dockerfile caches at the granularity of ordered instructions. Docker reuses a cached layer only when the instruction and all its inputs match a previous build — and invalidation is sequential, so one changed line rebuilds every layer after it. A well-ordered multi-stage Dockerfile (dependencies before source, build stage before a slim runtime stage) caches almost as well as anything: one measured comparison put cached multi-stage rebuilds at ~12s against ~15s for a single-stage equivalent. But the ordering discipline is manual, unenforced, and silently load-bearing. Put ARG GIT_SHA or a COPY . . above your dependency install and you have rebuilt Nixpacks' worst habit by hand.

Railpack exists because Railway measured all of the above and picked BuildKit's granularity. The Go rewrite generates a BuildKit LLB graph directly — the same low-level primitive Docker uses — instead of generating Dockerfiles around a Nix store. That buys explicit control over which layers survive into the final image (the multi-stage Dockerfile's discipline, automated) plus parallel stage execution and cache hits shareable across environments. Railway's headline deltas versus Nixpacks — 38% smaller base Node images, 77% smaller base Python images — are the size of the Nix-store tax being removed.

Cold builds: the Dockerfile wins the race nobody runs twice

Rank the builders on a from-scratch build of the same app and the order is unambiguous: hand-written Dockerfile first, Railpack second, Nixpacks third, buildpacks last.

The Dockerfile's ~15s against Nixpacks' ~87s on the same Next.js app is mostly Nix resolving and downloading packages fresh rather than pulling cached, pre-built layers. Railpack's independent 42s-against-76s shows how much of that gap was the Nix store rather than auto-detection itself: keep zero-config, drop Nix, halve the cold build. Buildpacks sit last on single-image speed in every head-to-head that includes them — a Spring Boot containerization benchmark ranks single-build speed Jib first, Dockerfile-plus-shell second, buildpacks a distant third — because detection and layer export are fixed costs no Dockerfile pays.

But a PaaS runs a cold build exactly once per dependency change and a warm build on every other push. Optimizing the builder for the first clock while ignoring the second is how you get a demo that feels fast and a fleet that burns build minutes. Which brings us to the clock that actually matters.

Warm builds and cache-hit rate: the clock a PaaS lives on

Define cache-hit rate the way a platform operator experiences it: across N successive deploys, what fraction of each rebuild is reused layers versus rebuilt work? On a code-only push — new commit, same dependencies — the builders diverge completely.

Buildpacks turn the most common fleet-wide event into a non-build. Heroku's engineering blog puts the rebase number plainly: pack rebase updates an image "in less than a second without rebuilding," against "hours" to rebuild every app's Dockerfile. Scale that to a fleet: a base-image CVE touching 500 apps is 500 sub-second layer swaps versus 500 full rebuilds. No other builder in this comparison has an equivalent primitive — it is the structural payoff of content-addressed layers, and it is why Render's buildpack bet is a cache-economics position, not nostalgia.

Dockerfiles hit or miss on author discipline. The cached ~12s multi-stage rebuild assumes the author ordered layers so the code change lands late. Across a fleet of tenant-authored Dockerfiles, that assumption fails constantly: COPY . . before RUN npm ci, build args injected early, secrets invalidating layers. A PaaS that defaults to Dockerfiles inherits every tenant's layer-ordering mistakes as its own build-minute bill — unless it lints or rewrites Dockerfiles, at which point it is maintaining a builder anyway.

Nixpacks misses almost by design. Same-store determinism plus per-deploy variable injection meant cache layers invalidated on nearly every deploy — warm builds that cost nearly as much as cold ones. This, more than image size, is the failure that killed it: a builder whose warm path barely beats its cold path has no economy of repetition, and repetition is the only thing a PaaS does at scale.

Railpack's warm path is its real thesis. The same-app operator numbers — 42s cold, 37s warm — look like a modest warmup until you compare them against Nixpacks' near-total invalidation: 37s warm against a 76s Nixpacks baseline is the build that finally gets cheaper the second time. Shared cache across environments extends the same logic sideways: staging's layers warm production's build.

Put fleet-scale arithmetic on it. A platform running 50 deploys a day, 22 working days a month, at 60s of builder time per deploy, burns ~18 builder-hours a month per app just repeating work. Cut warm rebuilds by two-thirds through layer reuse and the same fleet buys back two weeks of a builder machine per app per year — before counting the CVE-patch events where rebase collapses hours into seconds. The builder is not developer experience with caching attached; it is a caching system with developer experience attached.

What Railway and Render actually chose in 2026

The two vendors looked at the same table and made opposite bets — and both bets are coherent once you read them as cache-economics decisions.

Railway chose BuildKit granularity and automated Dockerfile discipline. Nixpacks is in maintenance mode; Railpack — Go, BuildKit-native, open source — is the default for new services. The rewrite fixed exactly the columns Nixpacks lost: image size (38% Node, 77% Python smaller), cold time (roughly halved on the same app), versioning granularity (exact major.minor.patch instead of whatever Nix's latest commit pins), and cross-environment cache sharing. The ecosystem is following: Coolify added Railpack as a beta build option, and self-hosted platforms like Pier shell out to it against a local BuildKit daemon. Railway's bet is that auto-detection was never the problem — the cache unit was.

Render chose buildpacks and kept the Dockerfile as the escape hatch. Render's native runtimes ride on Heroku-compatible buildpacks with a persistent build cache, while Docker support with multi-stage layer caching covers every app whose author wants to own the layers. Render never paid the Nix tax because it never built on Nix — and it keeps collecting the rebase dividend on every base-image patch. The bet sharpened in July 2026 when Cloud Native Buildpacks graduated within the CNCF, the foundation's highest maturity level: the "no Dockerfile needed" default now rests on a graduated-standard foundation rather than a single vendor's builder team.

Neither vendor defaults to raw Dockerfiles for everyone, and neither defaults to Nixpacks anymore. That consensus is the verdict: zero-config stays, but the cache unit underneath it has to be layers, not a store dump.

The verdict for a self-hosted PaaS default

If you run git-push builds on machines you own, the 2026 evidence points to a boring conclusion with one interesting footnote.

Default to Cloud Native Buildpacks (Paketo) when your scarcest resource is operator attention. Graduated CNCF standard, content-addressed layer reuse, sub-second fleet-wide CVE patching via rebase, and no per-tenant layer-ordering lottery. The cold-build overhead is real but it is paid once per dependency change, while the warm-path savings compound on every push and every patch Tuesday. For a small team running a PaaS as a side effect of running a business, the builder that patches 500 apps in seconds without rebuilding any of them is not a luxury — it is the difference between patching and postponing.

Default to Railpack when your scarcest resource is build minutes or image-pull bandwidth. Smaller images than CNB on the measured workloads, faster cold builds, and BuildKit-native caching without asking tenants to write Dockerfiles. The tradeoff is single-vendor governance and a younger ecosystem: Railpack is Railway's open-source project, not a graduated foundation standard, so you are tracking one company's roadmap.

Keep the hand-written Dockerfile as the escape hatch either way. Both defaults need it — for the tenant whose build genuinely needs custom stages, and as the cold-build ceiling no zero-config tool beats. Render's shape (managed default plus Docker when you ask) is the right shape regardless of which default you pick.

And Nixpacks' one surviving virtue is worth naming honestly: Nix-store determinism still gives the strongest "same inputs, same image" story of the four. If your threat model needs bit-reproducible builds more than it needs fast rebuilds — regulated artifacts, supply-chain forensics — Nix is a reproducibility tool wearing a builder costume, and judging it on cache-hit rate misses its actual job. Everyone else should judge builders on the warm path, because the warm path is where fleets live.

Builders are cache economics now

The 2026 scoreboard reads cleanly: Railway proved the Nix-store cache unit was the tax and rebuilt on BuildKit layers; Render proved the buildpack cache unit compounds and stayed; the CNCF stamped buildpacks graduated; and the hand-written Dockerfile remains unbeaten on cold builds and undefeated as an escape hatch. The through-line is that a git-push builder is a caching system first — cold time is a demo metric, warm time is a cost line, and image size is pull latency your users pay on every deploy.

Watch two things from here. First, whether Railpack's cross-environment cache sharing becomes the table-stakes feature every builder copies — shared warm layers across staging and production would move the fleet-wide hit rate more than any single-builder speedup. Second, whether rebase-style layer swapping escapes the buildpacks world: the day a Dockerfile-native flow can patch a base image without rebuilding app layers, the warm-path crown is genuinely contested. Until then, pick your default by your scarcest resource, keep the escape hatch open, and measure your builder the way your bill measures it — on rebuilds 2 through 200.

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