Skip to main content

Railway Killed Its Own Zero-Config Builder Over a 17x Image-Size Tax — Here's the Actual Bill

9 min readDora NodaDora Noda
Share

Railway built Nixpacks, ran it in production across 14 million app builds, and then quietly put it in maintenance mode — not because a competitor out-executed it, but because its own zero-config convenience was costing users an image-size tax nobody was tracking. One widely-cited before/after tells the story in two numbers: the same Next.js app came out of Nixpacks at 1.3GB and out of a hand-written Dockerfile at 76.83MB. Seventeen times smaller, for the exact same code.

That gap isn't a fluke of one misconfigured build. It's the structural cost of "push code, get a container" with nobody writing the Dockerfile — and it's worth pricing out precisely, because every git-push PaaS (Railway, Render, Coolify, Dokploy, and eventually any self-hosted alternative) makes the same bet on a tenant's behalf by default.

The bill: 1.3GB vs. 76.83MB, and 87 seconds vs. 15

Start with the concrete case, because it's the number that actually moved Railway to rebuild its builder from scratch. A developer running a standard Next.js app on Railway's Nixpacks builder got a 1.304GB production image. Switching to a hand-written, multi-stage Dockerfile — node:18-alpine as the base, Next.js's standalone output mode, and a final stage that copies over only public/, .next/standalone, and .next/static — dropped that to 76.83MB. Same app, same dependencies, 94% less image to push, pull, store, and scan.

Build time tells the same story from a different angle. Railway's own comparison of deployment methods puts a typical first Nixpacks build at roughly 87 seconds against 15 seconds for the equivalent Dockerfile build — a rough 6x gap driven mostly by Nix resolving and downloading packages fresh rather than pulling cached, pre-built layers. Cached rebuilds close some of that distance, but the first build (and the first build after a dependency bump) pays the full tax every time.

MetricNixpacks (zero-config)Hand-written Dockerfile
Image size (Next.js app)1.304 GB76.83 MB
First build time~87 sec~15 sec
Size reduction94% smaller (~17x)

Neither number is cherry-picked to make buildpacks look bad — they're the exact case Railway itself pointed to when explaining why it stopped developing Nixpacks further.

Why the bloat is structural, not a bug

The size gap traces to one architectural decision: Nixpacks builds on top of Nix, and Nix's package model dumps everything — compilers, headers, build-time tooling, the runtime libraries actually needed — into a single, undifferentiated /nix/store layer. There's no built-in mechanism to split "what the build needed" from "what the running app needs," so the final image ships both. A hand-written multi-stage Dockerfile does that split explicitly: one stage installs dependencies and compiles, a second stage copies over only the production artifacts, and everything else — including the entire toolchain — never makes it into the shipped image.

Nix's versioning model compounded the problem rather than offsetting it. Because Nix pins packages by commit rather than by semver, Railway found it could only realistically support the latest major version of a language runtime at a time — Nixpacks users chasing Node 24 or a specific Python patch release were out of luck, which is exactly what pushed Coolify to file its own migration issue in January 2026, citing Nixpacks' cap at Node 23 as the practical reason to move its self-hosted users onto Railway's successor tool instead. And because Railway injected deployment-specific variables into the build, cache layers invalidated on nearly every deploy — so even the builds that should have been fast, weren't.

Railpack halves the tax — it doesn't eliminate it

Railway's answer, shipped in beta starting March 2025 and now the default for new services, is Railpack: a full rewrite from Rust-plus-Nix to Go-plus-BuildKit. Instead of resolving packages through Nix's store, Railpack generates a custom BuildKit LLB graph directly — the same low-level build primitive Docker itself uses — which lets it parallelize stages and control exactly which layers survive into the final image, the same discipline a hand-written multi-stage Dockerfile applies manually.

The measured result: 38% smaller images for Node and 77% smaller for Python, versus the equivalent Nixpacks build. That's a genuine, substantial fix — a Node image that shipped at roughly 730MB under Nixpacks lands around 450MB under Railpack — and it also buys back the versioning granularity Nix cost Railway, since Railpack can target a specific major.minor.patch instead of "whatever Nix's latest commit happens to pin."

What it doesn't do is close the gap all the way to a hand-tuned Dockerfile. A 450MB auto-detected Node image next to a 76.83MB hand-written one is still roughly a 6x difference — Railpack fixes the worst of the structural bloat (the undifferentiated single-layer dump, the version pinning, the cache-busting), but it can't know, the way a human author can, that a Next.js app only needs its standalone output copied into a from-scratch runtime stage. Zero-config detection is bounded by what it can infer about your app; a developer who already knows their runtime's shape can always out-build it.

What the extra gigabyte costs after the build finishes

The build-time number gets the attention, but the image-size gap keeps charging rent long after the build log scrolls past. A 1.3GB image versus a 76.83MB one isn't just slower to produce — it's slower to push to a registry, slower to pull onto every node it scales out to, and larger on every disk that caches a copy. Scale a service from one replica to ten during a traffic spike and a platform pulling a gigabyte-plus image ten times is burning registry bandwidth and node-provisioning time a 77MB image wouldn't. Registry storage costs scale with image size directly, and so does the blast radius of every CVE scanner sweep — a bloated image built from an undifferentiated /nix/store layer ships build-time compilers and headers into production that a security audit has to account for even though the running app never touches them.

None of that is unique to Nix specifically — it's worth being honest that Cloud Native Buildpacks (Paketo, the CNCF-hosted alternative that predates both Nixpacks and Railpack) land closer to hand-written-Dockerfile parity in practice: a well-optimized Dockerfile and a Paketo build often come out comparably sized, and Paketo's per-buildpack layering is more granular than Nix's single-store dump even though it still trails a Dockerfile author who knows exactly which files their runtime needs. The honest ranking, worst to best on image size, is Nixpacks' old Nix-store model at the bottom, Railpack and Paketo in the middle doing real but partial cleanup, and a Dockerfile a developer actually tuned for their app at the top — with the caveat that an un-optimized, copy-everything Dockerfile can just as easily land back at the bottom of that list. Auto-detect isn't inherently worse than a Dockerfile; it's worse than a Dockerfile someone bothered to tune, which is the comparison that actually matters for a platform deciding what its default should look like.

What the self-hosted PaaS lesson actually is

This is where it stops being a Railway story and starts being an infrastructure-design question every git-push platform has to answer, self-hosted or not. Coolify's own users hit the same Nixpacks version ceiling Railway's did — the exact bug report that pushed Coolify toward adopting Railpack as an alternative builder confirms the tax isn't unique to Railway's fleet, it's inherited by anyone who bundled Nixpacks in rather than building their own detection layer.

For a platform like Bex.co, whose entire pitch is "push a git repo, get a running HTTPS service on machines you own," the tradeoff isn't whether to offer zero-config detection — every git-push PaaS has to, because most tenants will never hand-write a Dockerfile before their first deploy. It's how much of the resulting image-size and build-time tax to accept as the cost of that convenience, and how visibly to offer the escape hatch. The concrete numbers above suggest a specific default:

  • Auto-detect (Railpack-style BuildKit graph, not a Nix-store dump) as the on-ramp. A first-time deploy shouldn't require a Dockerfile — that's the whole point of "just push code" — but the detection layer should be BuildKit-native from day one rather than inheriting Nix's single-layer bloat, since retrofitting that fix after tenants depend on the image sizes is exactly the multi-year rewrite Railway just went through.
  • A visible Dockerfile override, not a buried one. The moment an app's dependency tree, cold-start latency, or registry storage bill starts to matter, a tenant needs a documented path to drop in their own Dockerfile and keep everything else — the deploy pipeline, TLS, the domain — unchanged. Treating auto-detect as a permanent default rather than a fast on-ramp is how a platform ends up owning a 17x-bloat problem it didn't have to.
  • Track base-layer drift the way kpack does for Cloud Native Buildpacks. Zero-config building only stays cheap if the base image and language runtime versions get rebuilt when a CVE lands upstream — not just when a tenant pushes new code. A detection layer that's fast today but never gets revisited is the same trap Nixpacks fell into with Node's major-only version pinning.

The number worth remembering isn't "buildpacks are bad" — Railpack's 38-77% cut proves the model can work. It's that "zero-config" was never free; it was a cost nobody had put a number on until a 1.3GB image next to a 76.83MB one made it impossible to ignore.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a Dockerfile always available the moment auto-detect stops being enough. 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