Docs
Building with Railpack
What Railpack detects, how to steer it, and what to do when a build fails. The official documentation at railpack.com is the reference; this page says what that means when your code is built on Maxlayer.
How it works#
railpack is the default build type. When you create an app it is what builds your code unless you choose a Dockerfile or static output instead. Railpack inspects the repository and works out how to build and start it: which language and framework it is, which versions to install, which commands build it, and which command runs it. The common cases need no configuration at all.
Each deploy builds from a fresh copy of your source on a server the platform manages. The Deployments tab in your app shows the log of what Railpack decided and did — read it when a build is not what you expected, because it tells you which provider was detected, which versions were installed, and which commands ran.
What you configure on the app — the source, the build path, the port, and the environment variables — is what this page describes. Anything deeper than that lives in the official getting-started guide.
Supported languages#
Railpack detects a language by the files in the directory being built. Nothing more than the file on the left is needed to be built as the language on the right.
| Language | Detected when |
|---|---|
| Node.js | A package.json exists. npm, yarn, pnpm and bun are all supported; SPA frameworks such as Vite, Astro, Next.js static export and Angular are detected and served with a web server of their own. |
| Bun | The packageManager field names bun, or a bun.lock / bun.lockb exists. Shares Node.js's framework and SPA support. |
| Python | main.py, app.py, start.py, bot.py, hello.py or server.py in the root; or a requirements.txt, pyproject.toml or Pipfile. pip, poetry, pdm, uv and pipenv are supported; Flask, FastAPI, Django and FastHTML get a start command of their own. |
| Go | A go.mod, go.work or main.go exists. Built as a static binary. |
| PHP | An index.php or composer.json exists. Served by FrankenPHP; Laravel projects (an artisan file) get migrations, storage links and cache optimisation. |
| Java | A gradlew or pom.xml exists. Gradle and Maven are supported, including Spring Boot. |
| Ruby | A Gemfile exists. Rails projects (a config/application.rb) get asset precompilation and database drivers. |
| .NET | Any *.csproj exists. Published with dotnet publish, and told to listen on the port your app expects. |
| Deno | A deno.json or deno.jsonc exists. A main.ts/main.js at the root becomes the entry point. |
| Rust | A Cargo.toml exists. Compiled to a binary. |
| Elixir | A mix.exs exists. Phoenix projects compile a release and run it. |
| Gleam | A gleam.toml exists. Built as an Erlang shipment. |
| C/C++ | A CMakeLists.txt or meson.build exists. |
| Static HTML | An index.html, a public directory, or a Staticfile exists. Served with a web server; a Staticfile can set the document root and SPA routing. |
| Shell scripts | A start.sh exists. The shebang line picks the interpreter. |
A project that matches several languages is built with whichever provider Railpack's detection decides — the deploy log names it. If detection picks wrong, force the provider in railpack.json.
Build path#
The build path is the directory Railpack inspects. It defaults to the repository root (/); set it to the subdirectory that holds your app when the repository is a monorepo. Everything on this page — detection, the configuration file, the Procfile, version files, .dockerignore — applies relative to that directory.
Port#
The port is the one thing Maxlayer asks that Railpack does not decide. Your process must listen on the port you set — the platform routes traffic to it. 3000 suits most frameworks; the app's port setting is where you say otherwise.
Start command#
Railpack decides how your app starts. The decision depends on the provider: a Node app's start script (then its main field), a Python main.py, a compiled binary for Go, Rust and .NET, the framework's own default where there is one. Read the deploy log to see what was chosen.
When the default is wrong, the override chain, strongest first:
- A
RAILPACK_START_CMDenvironment variable — set it in the app's Environment tab. deploy.startCommandin the configuration file.- A Procfile in the project root.
- Provider defaults.
Procfile#
A Heroku-style Procfile at the project root tells Railpack how the app starts. Each line names a process type and the command that runs it:
web: gunicorn --bind 0.0.0.0:3333 main:app
worker: celery worker -A myapp.celery
scheduler: celery beat -A myapp.celeryWhen a Procfile is present, the web entry wins, then worker, then the first other type — so one file can carry a web server and background jobs, and the container runs the right one. If neither web nor worker is defined, the first entry is used.
Configuration file#
A railpack.json at the root of the directory being built steers the build. It is optional — its job is to change parts of the plan Railpack generated, not to replace it.
Forcing the provider#
The provider field overrides language detection when it picks wrong. The values are node (covering Bun and frontend apps), python, golang, php, java, ruby, dotnet, deno, rust, elixir, gleam, cpp, staticfile and shell. Tool versions can be pinned alongside it:
{
"$schema": "https://schema.railpack.com",
"provider": "node",
"packages": {
"node": "22"
}
}The start command#
The deploy.startCommand field is the strongest in-repository way to set how the app starts:
{
"deploy": {
"startCommand": "node dist/index.js"
}
}Custom steps and commands#
The steps map replaces or extends the generated build steps. Commands come in four kinds — an exec command ( npm install), a PATH: entry, a COPY: entry, and a file command — and writing "..." as the first entry appends to the commands Railpack generated rather than replacing them:
{
"steps": {
"install": {
"commands": ["npm install"]
},
"build": {
"inputs": [{ "step": "install" }],
"commands": ["...", "./my-custom-build.sh"]
}
},
"deploy": {
"startCommand": "node dist/index.js"
}
}Steps copy files from each other or from images through inputs layers, each with include and exclude filters, and the deploy section assembles the final image the same way. Caches, declared at the root and attached to steps, speed up repeat builds and are never part of the image.
The official configuration documentation covers every field, layer type and cache option. Editors pick up the schema automatically via the $schema line, and comments are allowed.
Build variables#
A handful of RAILPACK_ variables change the build itself. Set them in the app's Environment tab like any other variable; they take effect on the next deploy.
| Variable | What it does |
|---|---|
RAILPACK_START_CMD | The command the container runs — overrides the provider default, the config file and the Procfile. |
RAILPACK_BUILD_CMD | Replaces the generated build-step commands. |
RAILPACK_INSTALL_CMD | Replaces the generated install-step commands. |
RAILPACK_PACKAGES | Extra Mise-managed tools, space-separated: jq@latest python@3.11. |
RAILPACK_BUILD_APT_PACKAGES | Extra system packages installed during the build. |
RAILPACK_DEPLOY_APT_PACKAGES | Extra system packages installed in the final image. |
RAILPACK_DISABLE_CACHES | Disables named build caches, or * for all of them. |
RAILPACK_VERBOSE | Verbose build logging — the first thing to turn on when a build fails in a way the log does not explain. |
The official environment-variable reference lists the rest, including the per-language version pins such as RAILPACK_NODE_VERSION and RAILPACK_PYTHON_VERSION.
Tool versions#
Railpack is built on Mise, so version files in your repository are honoured. A mise.toml or .tool-versions at the build root sets tool versions explicitly:
[tools]
node = "22"
python = "3.13"The single-language version files work too, and take priority where a provider says so: .node-version, .nvmrc, .python-version, .ruby-version, .go-version, .java-version, .deno-version, .bun-version, rust-toolchain.toml and global.json, among others. Some providers also read a version from the manifest — engines.node in package.json, go.mod, composer.json, Cargo.toml, the TargetFramework in a .csproj.
Excluding files#
A .dockerignore in the project root keeps files out of the build — local artifacts, secrets, anything the image does not need. The syntax is Docker's:
**/node_modules
**/.venv
.env
*.logA pattern without wildcards matches only at the root — node_modules excludes the top-level directory while **/node_modules excludes every one at any depth. The exclude field in railpack.json is merged with the .dockerignore, so a project can keep its standard ignore file and add build-specific rules beside it.
Nothing is excluded by default: without either file, everything in the build directory ships to the build. Keep .env files and local-only directories out — the official guide has a pattern list to start from.
When a build fails#
In order, when a deploy does not behave:
- Read the build log. It names the provider, the versions, and the command that failed. Turn
RAILPACK_VERBOSEon and redeploy if the default output does not say enough. - Check the port. A build that succeeds and a container that runs, with nothing served, is almost always the port.
- Check the build path. A monorepo whose app lives in a subdirectory needs the build path set, or Railpack inspects the wrong directory.
- Check your build script. An app that builds locally but fails here usually fails at its own build command — and a failure that reproduces locally is faster to iterate on.
The official resolving-errors guide and the language page for your stack — linked in the language table — cover the rest. If the log points at something the platform controls, tell us and quote the requestId from the failing deployment.
Testing locally#
Railpack is a command-line tool you can run yourself. The official developing-locally guide walks through building a project with the same Railpack that builds it here, and the CLI reference covers the flags. A project that builds locally is a deploy away from building here — and a failure that reproduces locally is a bug you can see rather than a log you have to read.
Something missing here?Tell us what you were looking for.