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.
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.celery
When 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.
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
*.log
A 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 .