Railpack vs Dockerfile — choosing how your repo becomes a container
Published 2026-05-08 by tavin
Two paths can take your Git repo to a running container:
- An auto-builder reads the repo, infers the runtime, and produces the image.
- You write a
Dockerfileand the platform builds that.
Railpack is the auto-builder tavin.cloud uses by default. It is the spiritual successor to Nixpacks and a contemporary of Cloud Native Buildpacks. All three solve the same problem: most apps do not actually need a hand-written Dockerfile, and the time it takes to write one for a small service is mostly wasted.
This post is about how to pick between the two paths and how a good platform lets them coexist.
What an auto-builder actually does
A modern auto-builder is, roughly:
- Inspect the repo for language and framework signals —
package.jsonfor Node,pyproject.tomlfor Python,go.modfor Go,Gemfilefor Ruby,Cargo.tomlfor Rust, etc. - Pick a base image (Alpine, Debian slim, distroless, depending on the builder).
- Install the runtime at the version the repo requests.
- Install dependencies via the lockfile-appropriate package manager.
- Run a build step if the repo declares one (
bun run build,npm run build,pip install,go build, …). - Set the start command, the listening port, and a sensible entrypoint.
- Output a container image.
Done well, this is invisible. You push the repo, you get a container. Done badly, you get the kind of mystery debugging where the build fails on the platform but works on your laptop and you cannot tell why.
Railpack’s bet is that an open-source, scrutable auto-builder is the right shape: the build plan is inspectable, the rules live in a public repo, and you can reproduce builds outside the platform.
When auto-detection is the right choice
Auto-detection is the right default when:
- The app is a single process listening on a port.
- The runtime is mainstream (Node, Python, Go, Ruby, PHP, Java/Kotlin, Rust, Bun, Deno).
- Dependencies are pure language packages —
npm,pip,cargo,go mod, etc. - You do not care exactly which base image or Linux distro the platform picks.
- You would otherwise be copying the same
Dockerfileacross every service in your org.
This is most internal services, most public APIs, and most early-stage products. The auto-detector is faster to set up and faster to maintain. There is nothing for a future engineer to have to read.
When a Dockerfile becomes load-bearing
A Dockerfile is the right choice when at least one of these is true:
- System packages. You need
libvips,ffmpeg,chromium,poppler-utils,libreoffice, or some other native dependency the auto-builder will not install. - Custom binaries. Your app shells out to a binary you compile yourself or download at build time.
- A precise base image. Compliance, supply-chain hardening, or distroless builds want a specific base, not whatever the auto-detector picked.
- Multi-stage caching. Your build is large enough that you want explicit stage boundaries to control cache hits.
- Identical images everywhere. The same image must run in dev, CI, staging, and production, byte for byte.
- Unusual entrypoints. A custom shell wrapper, init script, or tini-style PID 1 handler.
- Exotic runtimes. GPU containers, custom Node patches, Cap’n Proto servers, anything off the well-trodden path.
In these cases, a Dockerfile is not a workaround for a missing auto-builder feature — it is the right artifact. It writes the runtime down. It is reviewable in pull requests. It works on every platform that can build a Dockerfile.
The escape-hatch principle
The point isn’t “auto vs hand-written.” It is “auto by default, explicit when needed, on the same platform.”
A well-designed PaaS treats this as a hierarchy:
- No
Dockerfilein the repo → auto-build with Railpack. Dockerfileexists at the repo root → use it.- A pre-built image is referenced explicitly → skip the build, deploy the image.
The repo is the cloud contract, and the contract knows which path to take. You do not have to commit to “we are an auto-detected app” or “we are a Dockerfile app” forever — you can start with auto-detection, add a Dockerfile the moment you need a system package, and the platform picks up the change without further configuration.
This is also why Buildpacks struggled in places where teams needed escape hatches but did not want to abandon the buildpack ecosystem entirely. The escape hatch needs to be obvious, supported, and not punishing to use.
What “good” looks like in 2026
A useful PaaS in 2026:
- Auto-detects the common stacks and produces a normal OCI container.
- Honors a Dockerfile when one exists, without making the user toggle a setting.
- Streams build logs for both paths so you can debug a failed build the same way regardless of who wrote it.
- Runs the resulting container as a normal Linux process — no proprietary runtime shim, no opaque “we wrap your app” layer.
- Caches build layers between deploys so iteration is fast.
- Lets the agent inspect the build plan so an AI coding agent can reason about how the repo will actually build before pushing.
That last point is increasingly relevant. AI agents — Claude Code, Cursor, Codex — are now common participants in the workflow. An agent asked to “fix the build” needs to understand what the platform did, which step failed, and what the next change should be. A well-structured build plan is a much better surface than a colorized terminal log.
This is also why we treat MCP as the wire format for deployment rather than a CLI scrape: the agent sees the build plan as structured data.
A practical recommendation
Default to auto-detection. Add a Dockerfile the day you need one. Pick a platform that supports both paths cleanly.
Concretely, on tavin.cloud:
- A new Node, Bun, Python, Go, Ruby, or PHP service deploys with Railpack and zero hand-written build config. See the quickstart for the full path.
- Adding a
Dockerfileat the repo root switches the build to that Dockerfile on the next push, no toggle, no migration. - A pre-built image (e.g. from your own registry) is supported when you would rather not build at all.
There is no “Dockerfile mode” or “auto mode” lock-in. The repo decides, the platform respects it, and the artifact at the end of every path is the same: a normal Linux container running on Kubernetes that you do not have to operate.
If you want to compare how this same dual-path contract works elsewhere, our side-by-side comparisons cover Render, Railway, Fly.io, and Vercel. They are written from our perspective — read with that bias in mind.
The right answer for any single repo is usually “start auto, switch to Dockerfile when you have a reason.” The right answer for the platform is “support both, equally well, on the same primitives.”