Skip to main content

Railway Retired Nixpacks for Railpack: What Changed in the Zero-Config Build Path

11 min readDora NodaDora Noda
Share
On this page

Railway has now replaced its zero-config builder twice. First Nixpacks displaced Heroku buildpacks as the thing that turned git push into a running container; then, in March 2025, Railway announced Railpack as the ground-up successor to Nixpacks — and Nixpacks is today in maintenance mode, with Railway's docs and builder options pointing at Railpack. That churn is not indecision. It is fourteen million builds worth of data about what breaks when auto-detection meets real-world repos at scale.

The headline numbers tell the story before any architecture does: Nixpacks worked well for roughly 80% of users, which still left around 200,000 Railway users hitting its limits; Railpack cut base image sizes by 38% for Node and 77% for Python; and Railway framed the rewrite as the builder upgrade needed to scale from 1 million users to 100 million. The short version of what changed:

Build pathNixpacksRailpack
LanguageRustGo (for the BuildKit libraries)
Package sourceNix store (/nix/store)Mise for version resolution and install
Version pinningApproximate, commit-SHA tiedExact major.minor.patch, locked after green builds
Image constructionEmits a DockerfileCustom BuildKit LLB frontend, no Dockerfile in the middle
Build modelPlan → Dockerfile → buildAnalyze → Plan (JSON) → BuildKit graph
Config filenixpacks.tomlrailpack.json (no compat layer — old file is ignored)
CachingCoarse, single giant layerPer-step layers, env-hash invalidation, caches shared across environments
SecretsEnv vars in buildBuildKit secrets, kept out of logs and layers

If you operate or are building a PaaS — especially a Render-compatible, self-hosted one where "push a repo, get an HTTPS service" has to work with zero configuration — this is the decision you cannot dodge either: adopt someone's detector, adopt Cloud Native Buildpacks, or hand-roll your own. Railway just showed you, twice, what each round of that decision costs.

What Nixpacks was — and why Railway built it

Nixpacks launched in 2022 with a one-line formula: app source plus Nix packages plus Docker equals image. Railway described it as pulling ideals from both nixpkgs and Cloud Native Buildpacks, with the goal of making reproducibility approachable: providers auto-detect the repo's language and framework, resolve system and language dependencies from the Nix ecosystem, and generate a Dockerfile that gets built into an OCI image. Custom packages arrive via environment variables like NIXPACKS_PKGS, and per-repo tuning lives in nixpacks.toml.

It worked well enough to escape Railway. Fly.io added fly deploy --nixpacks in July 2022, Coolify made Nixpacks its default auto-detect builder, and self-hosted platforms like ZaneOps standardized on it — "Nixpacks figures it out" became the default answer to zero-config builds. On Railway itself it became the default path from user code to image, replacing Heroku buildpacks entirely, and went on to build more than 14 million apps.

That adoption matters for what comes next: when Railway says Nixpacks hit limits, it is not describing a prototype. It is describing the most battle-tested zero-config builder in the PaaS world outside Cloud Native Buildpacks itself.

Why Nix broke at Railway's scale

Railway's announcement post, "Why We're Moving on From Nix," names three failure modes, and each one is a version of the same lesson: the thing that makes a builder pleasant at small scale is exactly what makes it dangerous at large scale.

Versioning was approximate and coupled. Nix ties package versions to commits in the nixpkgs repo, with only the latest major version of each package available. Railway tried to support every patch version anyway, which produced exactly the kind of mapping table that makes maintainers flinch — version strings hand-paired to commit SHAs:

text
("5.4.2", "c82b46413401efa740a0b994f52e9903a4f6dcd5"),
("5.5.2", "7592790b9e02f7f99ddcb1bd33fd44ff8df6a9a7"),
("5.5.3", "7cf5ccf1cdb2ba5f08f0ac29fc3d04b0b59a07e4"),

For Node and Python, Railway gave up and supported only the latest major — NIXPACKS_NODE_VERSION accepts 22, not 22.12.0. Worse, because every version resolves through one commit SHA, bumping that SHA to support a new package version silently moved every other package too. A default-version change could break previously green builds with unexpected errors. Railway's own summary is the quote to remember: they feel bad when users can't access the latest packages, but worse when previously functional builds suddenly fail.

Images were bloated by construction. Nixpacks pulled dependencies through one giant /nix/store layer containing everything both the build and the runtime needed, with no way to split it into separate layers. Final images carried build toolchains into production. This was not a Nix flaw per se — Railway is explicit that it was a flaw in how Nixpacks used Nix — but the effect was the same: slow pushes, slow pulls, slow deploys.

Caching was coarse. With one monolithic dependency layer and a linear Dockerfile model, cache hits were all-or-nothing. Change one environment variable and you rebuilt the world.

None of these is fatal for a side project. All three are fatal for a platform promising one-click deploys to hundreds of thousands of developers who will never read a build log unless something is already on fire.

What Railpack changes: the full before/after

Railpack, announced March 4, 2025 and open-sourced at railpack.com, keeps the zero-config promise and replaces nearly every mechanism behind it. The pipeline is now three stages: Analyze (inspect the code, decide packages, install/build/start commands), Plan (emit a JSON-serializable build plan where each step declares its inputs from prior steps or images), and Generate (turn the plan into a BuildKit LLB graph and solve it). Dockerfiles are linear; BuildKit graphs are parallel, with each command in its own stage and precise control over input layers and final filesystem assembly.

Row by row, against the failure modes above:

  • Versions become exact and locked. Mise handles version resolution and most package installation, supporting real major.minor.patch versions instead of Nix's approximations — including the latest language releases with no Railpack release required. And Railpack locks the dependencies used by a successful build, so a default-version bump (say, Node 22 to 24) cannot retroactively break apps that built fine before. That single feature directly answers the worst Nixpacks failure mode.
  • Images get assembled, not inherited. The custom BuildKit LLB frontend gives Railpack full control over layers and the final filesystem: build tooling stays in build stages, runtimes stay lean. Railway reports 38% smaller base Node images and 77% smaller base Python images versus Nixpacks.
  • Caching gets surgical. Railpack talks to BuildKit directly, so caches hit per-step and can be shared across environments. To invalidate correctly when config changes, it hashes the used environment variable values and mounts the hash into the input filesystem — same code plus same variables equals a cache hit, no more, no less.
  • Secrets stop leaking. Build-time secrets ride BuildKit secrets, keeping them out of build logs and final images — a real improvement over env-var-soup builders.
  • Static sites become first-class. Vite, Astro, Create React App, and Angular deploy with zero config, which quietly makes the builder cover frontends as well as backends.

Two migration facts deserve emphasis because they foreshadow the lesson for other platforms. First, the rewrite moved from Rust to Go specifically for the BuildKit libraries — the ecosystem around your image-construction layer matters more than language preference. Second, there is no Nixpacks compatibility layer: under Railpack, nixpacks.toml is silently ignored, and configuration moves to railpack.json plus RAILPACK_* environment variables. Real-world migration commits show users discovering that the old file is dead weight only after builds misbehave. Railway bought a clean architecture by spending its users' migration effort — a tradeoff worth naming honestly.

Language coverage started narrow on purpose — Node, Python, Go, PHP, and static HTML at beta — and has since grown to include Java, Ruby, and framework-aware static support. Railway said it would prioritize depth on commonly used languages over breadth until the core API and abstractions were nailed. That sentence is doing a lot of quiet work, as the next section shows.

Why Railway still didn't pick Cloud Native Buildpacks

Here is the genuinely interesting decision. Railway has now rebuilt its zero-config layer twice — Heroku buildpacks to Nixpacks, Nixpacks to Railpack — and both times it built in-house rather than adopting Cloud Native Buildpacks, the CNCF project with the Paketo builders that powers Heroku's next-generation stacks and Google Cloud's gcr.io/buildpacks/builder. Why?

The announcement never argues CNB is bad. It argues, implicitly throughout, that Railway needed control CNB's generality cannot give it: a custom LLB frontend that fully defines how an image is made, build state locked per app, caches shared across a project's environments, and tight integration between builds and the Railway UI. CNB gives you a standard lifecycle (detect, analyze, build, export) running inside composable builders — excellent for portability, but the image-assembly decisions live in the builder and lifecycle, not in the platform. Railway wanted those decisions. When your roadmap says "scale the builder from 1M to 100M users," owning the layer graph is the feature.

There is also a product-shape argument. CNB optimizes for ecosystem breadth: anyone can write a buildpack, Paketo covers the long tail. Railway optimizes for depth on the frameworks its users actually deploy, with PaaS-embedded behavior — per-service railpack.json, RAILPACK_* env vars, static-site handling wired into Railway's deploy model — that a generic lifecycle would have to bolt on. Twice, Railway priced the maintenance of its own detector below the cost of bending a standard to its UX.

The honest counterweight: CNB remains the most mature, most portable option, and Railway's choice is not a verdict on it. Heroku and Google Cloud ship CNB at enormous scale. What Railway's churn actually proves is narrower but still useful — a platform whose core promise is "push code, get a service" will eventually find its builder is product surface, not plumbing, and product surface gets owned. The question for any other platform is when, not whether, that bill comes due.

What this means for a Render-compatible self-hosted PaaS

If you run a Render-compatible platform on machines you own — which is exactly the bet Bex.co makes — "push a git repo, get a running HTTPS service" has to work with zero configuration on day one, and Railpack's arrival changes the build-vs-buy math in three concrete ways.

Option one, adopt Railpack, is newly credible. It is open source, Go-native, BuildKit-based, and already escaping Railway the way Nixpacks did: Coolify's release notes list Railpack as a beta build-pack option, ZaneOps is tracking Railpack builder support, and self-hosted builders are wiring it in as a BuildKit frontend that replaces both Dockerfile generation and image building in one tool. For a young platform, starting from Railpack's Analyze→Plan→Generate pipeline beats starting from an empty providers/ directory. The caveats are the ones Railway stated itself: coverage prioritizes popular-language depth over breadth, the project is young, and its roadmap answers to Railway's needs first.

Option two, CNB/Paketo, is the stability pick. You inherit a maintained long tail of language support and genuine portability — the same build runs on Heroku, Google Cloud, and your cluster. You pay in control: the layer graph, caching behavior, and UX seams belong to the lifecycle, and PaaS-specific behavior gets bolted on rather than built in.

Option three, hand-rolling, now has a spec. Whatever you build, Railway's post is the acceptance criteria: exact version resolution with locked green builds, build/runtime layer separation, per-step caching with correct invalidation, secrets that never touch logs or layers, and a migration story for the config file you will inevitably replace. If your custom detector cannot check those boxes, you are rebuilding Nixpacks' failure modes, not Railpack's fixes.

Whichever option you take, budget for the migration Railway didn't provide. The nixpacks.toml-silently-ignored episode is the cheapest lesson in this whole story: when you replace a builder, the old config must error loudly, not sit inert while users debug phantom behavior. Deprecation UX is builder UX.


Railway replacing its builder twice in three years looks like churn until you read the build counts. Fourteen million apps taught it that approximate versions break trust, monolithic layers tax every deploy, and the builder is the most-touched code path a PaaS owns. Railpack is that tuition converted into architecture: exact versions, locked builds, a layer graph the platform fully controls, and caching designed for a multi-environment world. The next platform to face the buildpack-vs-custom-detector decision starts with better options than Railway ever had — including, fittingly, Railway's own answer, open-sourced.

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