Choosing Buildpacks vs Dockerfile on Railway: A Practical Decision Guide
When deploying to Railway, you can rely on auto‑detected Buildpacks or hand‑craft a Dockerfile. This guide weighs detection quirks, build speed, customization limits, and migration paths to help you pick the right method for your project.
06 Dec 2025, 20:43 UTC

What Decision Are We Facing?
When you push a repository to Railway, the platform must build a container image for each service. You can let Railway auto‑detect the language using its Buildpacks (CNB/Nixpacks), or you can supply a Dockerfile to take full control of the build. The decision hinges on:
- Detection reliability – Will Railway pick the right language?
- Build speed and caching – How fast will subsequent deployments run?
- Customisation depth – Do you need custom base images, multi‑stage builds, or system packages?
- Migration effort – Can you switch later, and what cleanup is required?
- Team skillset – Are developers comfortable with Dockerfile syntax?
Option Comparison
| Feature | Buildpacks (Nixpacks) | Dockerfile |
|---|---|---|
| Detection | Auto‑detect via lockfiles (package.json, requirements.txt, go.mod, etc.) | Explicit – you decide the base image |
| Build speed (first deploy) | Fast – cached builder images, minimal layers | Depends – single‑stage may be slower, multi‑stage adds overhead |
| Incremental deploy speed | Slower if lockfile changes; otherwise fast via CNB stack caching | Fast when dependencies unchanged; Docker layer cache effective |
| Customization depth | Limited to language/runtime, system packages via Nixpacks, no multi‑stage | Full control: base image, ARGs, RUN steps, multi‑stage, COPY, etc. |
| System package installation | Via Nixpacks (nixpkgs), pinned to builder version | Manual RUN apt‑get or equivalent |
| Environment variables at build time | CNB_BUILD_ENV, CNB_STACK_ID automatically set | Only those defined with ARG or ENV in Dockerfile |
| Runtime environment variables | Same as Dockerfile – Railway injects all RAILWAY_* vars | Same |
| Rollback | Immutable image – rollback restores previous image hash | Same |
| Migration path | Switching to Dockerfile is one‑way; remove file to revert (clear cache) | Switching back to Buildpacks requires deleting Dockerfile and clearing cache |
| Monorepo support | Requires railway.json per service with rootDirectory, build.builder="NIXPACKS" | Natural – separate Dockerfile per service directory |
Trade‑Offs in Practice
- Speed vs Flexibility – If your app is a simple Node.js or Python web service, Buildpacks give you a quick, zero‑config deploy. If you need a custom runtime or native binaries, Dockerfile wins.
- Cache behaviour – Buildpacks rely on lockfile changes; a small tweak to package.json will rebuild all node_modules. Docker’s layer cache can keep node_modules untouched if you use a separate COPY step for lockfiles.
- System packages and CA certs – Buildpacks expose system packages via Nixpacks but with fixed versions. Dockerfiles let you install exact versions or copy custom certificates.
- Security context – Dockerfiles run as root during build by default; Railway drops privileges at runtime. Buildpacks run under a non‑root builder user, reducing accidental permission leaks.
Concrete Implementation Example
Scenario
Deploy a minimal Express.js service that needs a custom system package (e.g., libpq-dev) and a specific Node.js version (18.x). We’ll show both Buildpacks and Dockerfile approaches.
1. Buildpacks (default Nixpacks)
- Create
railway.jsonin the service root:{ "build": { "builder": "NIXPACKS", "rootDirectory": "." } } - Ensure
package.jsonlists Node.js 18.x inengines:{ "engines": { "node": "^18.0.0" } } - Add a
buildpacks.ymlto request the system package (optional, Nixpacks automatically includes many packages, but you can pin a version):packages: - name: libpq-dev version: 15.4-1 - Push to Railway; the log will show:
Using Nixpacks builder
Detected Node.js 18.x
Installing libpq-dev
Building image…
Runtime variables are injected automatically. To inspect the base image, run:
railway run --service my-express -- cat /etc/os-release
Output will show the CNB stack (Ubuntu 22.04).
2. Dockerfile
- Create
Dockerfilein the service root:# Build stage FROM node:18-slim AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . # Runtime stage FROM node:18-slim WORKDIR /app COPY --from=build /app . RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* EXPOSE 3000 CMD ["node", "index.js"] - Push to Railway. The log will contain:
Using Dockerfile
Building image…
To verify the base image, run:
railway run --service my-express -- cat /etc/os-release
It will show Debian slim (buster or bullseye depending on the tag).
Verifying Build Speed
After the initial deploy, modify only src/app.js and redeploy. Observe the Build Time metric in the Railway dashboard. Typically:
- Buildpacks may take 30–45 s if
package.jsonunchanged but node_modules are rebuilt. - Dockerfile with multi‑stage and separate COPY of lockfiles often completes in 10–20 s thanks to layer cache.
Migrating Between Methods
Switching from Buildpacks to Dockerfile is as simple as adding a Dockerfile to the service directory. Railway will detect the file and override the builder. The reverse requires:
- Remove the
Dockerfile. - Clear Railway’s build cache via the dashboard or CLI
railway cache clear --service my-express(requires admin). - Re‑push; Railway will resume Buildpack detection.
Note that the service remains in the Dockerfile state until the cache is cleared; otherwise, Railway will continue to use the last Dockerfile.
Practical Checklist Before Deciding
- Does the app rely on a language lockfile alone? Buildpacks is safe.
- Do you need a specific base image (e.g., Alpine, custom Debian)? Dockerfile is required.
- Do you need multi‑stage builds or complex build steps? Dockerfile.
- Is build speed critical for frequent tiny changes? Test both; Docker may win with layer cache.
- Do you want the simplest onboarding for new developers? Buildpacks.
Limitations & Caveats
- Buildpack detection can misfire in monorepos with multiple lockfiles; explicitly set
build.buildCommandor use Dockerfile. - Upgrading Railway’s builder may change available Nixpkgs package versions; test before major releases.
- Dockerfile builds run as root during build; ensure your Dockerfile does not create files with root‑only permissions that break at runtime.
- Private registry authentication for Dockerfiles requires
DOCKER_REGISTRY_USERandDOCKER_REGISTRY_PASSWORDenv vars; Buildpacks use Railway’s internal registry. - Railway free tier limits build minutes; complex Dockerfiles can consume more minutes than optimized Buildpacks.
Conclusion
Use Buildpacks when you want zero configuration, quick deploys, and the supported languages match your project. Opt for a Dockerfile when you need full control over the build environment, system packages, or a custom base image. Both methods produce immutable images and identical runtime variable injection, so the choice is purely about build-time flexibility and speed. Test both in a small branch, compare build metrics, and pick the one that aligns with your team’s expertise and deployment cadence.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.