# Railpack: building a container image without writing a Dockerfile

> Railpack is Railway's Go tool for turning a source tree into an optimised container image with no configuration, by detecting the project type and generating the build. It is the successor to Nixpacks, carries the lessons of running Nixpacks in production, and drives BuildKit rather than a Docker daemon.

**railwayapp/railpack** — Zero-config application builder that automatically analyzes and turns your code into an image

- Repository: https://github.com/railwayapp/railpack
- Website: https://railpack.com
- Stars: 1,262 · Forks: 178
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/railwayapp-railpack

## The successor to Nixpacks, carrying its production lessons

Railpack is a tool for building images from source code with minimal configuration, and the repository description calls it a zero-config application builder that automatically analyzes and turns your code into an image. Its lineage matters, because the README says plainly that it is the successor to Nixpacks and incorporates many of the learnings from running Nixpacks in production at Railway for several years. That is a much stronger signal than a greenfield project, since the failures that shaped this design are the ones a platform operator actually hit. The audience is split in two, and the documentation says so: there is a set of documentation for operators, meaning platforms like Railway that run the builder, and a set for users, meaning developers using Railpack to build their applications, and both are on railpack.com. The design assumption is that a project should be buildable with nothing more than the source tree, so the tool has to recognise what the project is and make reasonable choices about how to build and how to start it.

## The build runs on BuildKit, not on a Docker daemon

The dependency list tells you the architecture, and the most important entry is moby/buildkit at version 0.34.0. Railpack does not shell out to a Docker daemon to run the build; it drives BuildKit, and the local setup starts a BuildKit container for that purpose. The command is a privileged, detached, named container:

```bash
docker run --rm --privileged -d --name buildkit moby/buildkit
```

and the tool is told where to find it through an environment variable:

```bash
export BUILDKIT_HOST='docker-container://buildkit'
```

That is the single most important operational fact in the README. The build is reproducible in the sense that BuildKit gives you content-addressed caching and parallel execution, but it is not free: you are running a privileged container to do it, and on a shared or locked-down machine that is a decision with consequences. The rest of the dependency list is consistent with a serious builder rather than a script. There is containerd/platforms for platform handling, docker/cli, the OCI image-spec, and the opencontainers ecosystem, plus urfave/cli for the command line, BurntSushi/toml and yaml.v2 for configuration, Masterminds/semver for version comparisons, bmatcuk/doublestar and moby/patternmatcher for matching files, and a set of terminal packages from charmbracelet and muesli that are used to render the build output for a human.

## Getting the tool and building a first image

The install route is mise, and the getting started sequence is the whole tutorial. You install mise and then railpack itself through it:

```bash
curl -sSL https://mise.run | sh
mise install github:railwayapp/railpack@latest
```

After that comes the BuildKit container and the BUILDKIT_HOST export shown above, then you create an application and build it:

```bash
npm create next-app@latest my-app
cd my-app
railpack build .
```

Railpack detects the project type, which in this case is Next.js, and generates an optimised container image, with many languages and frameworks supported. The image is then run like any other:

```bash
docker run -p 3000:3000 -it my-app
```

The README adds an important caveat about the scope of those steps. They are for running Railpack locally to experiment and test. If you deploy on Railway, Railpack runs automatically when you push changes to your repository. That split is the strategic point of the project: the same builder is the local tool and the platform's build system, and the local path exists so you can reproduce a platform build without pushing.

## The example directory is the language matrix

The examples/ directory in the repository is the most useful part of the tree for an adopter, because each entry is a worked case of the detection logic. The names cover the breadth of what the detector handles: bun-pnpm, config-file, cpp-cmake, cpp-meson, deno-2, dockerignore-planner, dockerignore, dotnet-api, dotnet-cli, elixir-ecto, elixir-latest, elixir-phoenix, exclude-merge, expo-spa, three Gleam cases, go-cmd-dirs, go-mod, go-workspaces, java-gradle, java-maven, java-zulu-version, mise-config and node-angular. Reading them tells you two things. First, the supported surface, which spans Node, Go, Java, .NET, Elixir, C++, Deno, Expo and Gleam. Second, the shape of the escape hatches, since names like exclude-merge, dockerignore, dockerignore-planner and config-file are all about controlling what the build does rather than what it detects. The java-zulu-version entry is a good example of the kind of detail the examples capture, which is a specific JDK distribution, and that is the sort of thing a zero-config tool has to be told rather than guessed.

## Configuration, schema and the mise connection

Zero configuration does not mean no configuration, and the pieces in the tree show how a project overrides the defaults. There is a dedicated examples/config-file/ case, and the dependency list includes invopop/jsonschema, which is how a configuration file gets a schema, so an editor can validate and complete it. The versioning story comes from Masterminds/semver, which is what a build system needs to compare the toolchain version it found in the project against the one it wants to use. The mise-config example and the whole .mise/ directory point at the other half of the story, since the repository itself is developed with mise, holding mise.toml, mise.dev.toml, mise.ci.toml and the matching lock files, and the project ships a railpack.plugin.zsh for shell users. The combination means a project can pin its toolchain with mise, and Railpack will honour it, which is how a zero-config builder stays predictable when the underlying runtime has several versions in play.

## A Go monorepo with snapshot tests and goreleaser

The repository layout shows a project with a large engineering surface. There is core/ for the detection and build logic, buildkit/ for the BuildKit integration, cli/ and cmd/ for the command line, internal/ for the rest, images/ for image assets, integration_tests/ for the end-to-end cases, and docs/. The Go module is github.com/railwayapp/railpack requiring Go 1.26.8, and the test tooling is deliberate: gkampitakis/go-snaps for snapshot testing, stretchr/testify and google/go-cmp for assertions, and mvdan.cc/sh for running shell. Snapshot testing is the right choice for a builder whose output is a long, structured plan, since the interesting regression is a change in the generated build steps rather than a change in a return value. Release and development tooling is mise-based with a .goreleaser.yml for the binaries, an install.sh, a RELEASE.md, a SKILL.md, and both AGENTS.md and CLAUDE.md for automated contributors. The releases are frequent, v0.41.0 on 2026-10-13, v0.40.1 on 2026-09-28 and v0.40.0 on 2026-09-24, with the last push on 2026-10-15.

## Where Railpack is the wrong tool

Four limits are clear. The first is that a Dockerfile is the escape hatch and the tool knows it. The dependency on the opencontainers image-spec and the presence of a config-file example say that the output is a normal OCI image, and a project with a build that does not fit the detected pattern will need to say so in configuration, at which point you are maintaining a build description again. The second is that the local path needs a privileged BuildKit container, which is a heavier requirement than a plain docker build and a poor fit for a locked-down CI runner. The third is the trust question. Railpack decides what to install and what to run in your image, based on detection, so a project that depends on a specific runtime version or a specific system package is at the mercy of the detector, and the mitigation is the configuration file, which is the thing the tool promised to remove. The fourth is scope. It builds images and it detects projects; it does not deploy them, and on Railway the deployment story is the platform's rather than the tool's. If you want a build system whose behaviour you fully control, a Dockerfile is still the more predictable choice, and Railpack's value is highest for the long tail of services where nobody wants to own a Dockerfile.

## Conclusion

Adopt Railpack if you want container images without maintaining a Dockerfile for every service, and if you are willing to let a detector choose the build steps for you. Do not adopt it as a way to escape Dockerfiles entirely, because a project with unusual build requirements will still need the escape hatches, and the configuration file exists precisely for that. Before you switch, read the example directory for your language and check the documented configuration, and note the deployment story: on Railway the build runs automatically when you push, while elsewhere you are running BuildKit yourself, which is the part with the operational weight.

## FAQ

### What is Railpack?

It is a Go tool for building images from source code with minimal configuration, described as a zero-config application builder that automatically analyzes your code and turns it into an image. It is the successor to Nixpacks and is MIT licensed.

### How do I install and run Railpack locally?

Install mise with `curl -sSL https://mise.run | sh` and then `mise install github:railwayapp/railpack@latest`, start a BuildKit container with `docker run --rm --privileged -d --name buildkit moby/buildkit`, export BUILDKIT_HOST as docker-container://buildkit, then run `railpack build .` in your project and run the resulting image.

### Does Railpack use Docker or BuildKit?

BuildKit. The Go module depends on moby/buildkit, and the local setup starts a privileged moby/buildkit container and points the tool at it with the BUILDKIT_HOST environment variable set to docker-container://buildkit.

### Which languages and frameworks does Railpack support?

The examples directory covers bun-pnpm, cpp-cmake, cpp-meson, deno-2, dotnet-api, dotnet-cli, elixir-ecto, elixir-phoenix, expo-spa, gleam, go-mod, go-workspaces, java-gradle, java-maven, node-angular and more. The README states that many languages and frameworks are supported.

### How does Railpack differ from Nixpacks?

Railpack is the successor to Nixpacks and incorporates many of the learnings from running Nixpacks in production at Railway for several years. On Railway, Railpack runs automatically when you push changes to the repository, while the local steps exist for experimenting and testing.

## Sources

- [License: MIT](https://github.com/railwayapp/railpack/blob/main/LICENSE)
- [Project website](https://railpack.com)
- [railwayapp/railpack on GitHub](https://github.com/railwayapp/railpack)
- [README](https://github.com/railwayapp/railpack/blob/main/README.md)
- [Releases](https://github.com/railwayapp/railpack/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/railwayapp-railpack
