Node.js
Railpack builds and deploys Node.js applications with support for various package managers and frameworks.
Detection
Section titled “Detection”Your project will be detected as a Node.js application if a package.json file
exists in the root directory.
Versions
Section titled “Versions”Node.js
Section titled “Node.js”The Node.js version is determined in the following order of priority:
- Set via the
RAILPACK_NODE_VERSIONenvironment variable - Read from
devEngines.runtimeinpackage.jsonthrough Mise’s idiomatic file parsing - Read from the
engines.nodefield inpackage.json - Read from the
.nvmrcfile - Read from the
.node-versionfile - Read from
mise.tomlor.tool-versionsfiles - Defaults to
lts
This version resolution logic is applied consistently across all scenarios where Node is needed, including when Bun is the primary package manager but Node is required for native module compilation.
We officially support actively maintained Node.js LTS versions. Older versions of Node.js will likely still work but are not officially supported.
Node.js GPG verification is disabled by default; see the GPG verification recommendation to enable it in your project.
Package Manager Versions
Section titled “Package Manager Versions”The detected package manager’s version is determined in the following order:
package.json, through Mise’s idiomatic file parsing described below.- The package manager’s
enginesfield, such asengines.pnpm. - The detected lock file, when its format identifies a compatible version.
- The default version for the detected package manager.
Mise Idiomatic File Parsing
Section titled “Mise Idiomatic File Parsing”For Node.js, npm, Yarn, pnpm, and Bun, Mise parses package.json as an
idiomatic version file.
For Node.js, Mise reads devEngines.runtime.version when
devEngines.runtime.name is node.
For package managers, Mise checks these fields in order:
devEngines.packageManager: Usesversionwhennamematches the detected package manager. The value may be an object or an array, in which case Mise reads the first entry.packageManager: Parses<package-manager>@<version>and removes an optional+hashsuffix.
Railpack parses additional package manager version sources as described in Versions.
Runtime Variables
Section titled “Runtime Variables”These variables are available at runtime:
NODE_ENV=productionNPM_CONFIG_PRODUCTION=falseNPM_CONFIG_UPDATE_NOTIFIER=falseNPM_CONFIG_FUND=falseNPM_CONFIG_FETCH_RETRIES=5YARN_PRODUCTION=falseCI=trueConfiguration
Section titled “Configuration”Railpack builds your Node.js application based on your project structure. The build process:
- Installs dependencies using your preferred package manager (npm, yarn, pnpm, or bun)
- Executes the build script if defined in
package.json - Sets up the start command based on your project configuration
Railpack determines the start command in the following order:
- The
startscript inpackage.json - The
mainfield inpackage.json - An
index.jsorindex.tsfile in the root directory
Config Variables
Section titled “Config Variables”| Variable | Description | Example |
|---|---|---|
RAILPACK_NODE_VERSION |
Override the Node.js version | 22 |
RAILPACK_NO_SPA |
Disable SPA mode | true |
RAILPACK_SPA_OUTPUT_DIR |
Directory containing built static files | dist |
RAILPACK_PRUNE_DEPS |
Remove development dependencies | true |
RAILPACK_NODE_NPM_INSTALL |
Custom npm install command | npm ci |
RAILPACK_NODE_PRUNE_CMD |
Custom command to prune dependencies | npm prune --omit=dev --ignore-scripts |
RAILPACK_NODE_INSTALL_PATTERNS |
Custom patterns to install dependencies | prisma |
RAILPACK_ANGULAR_PROJECT |
Name of the Angular project to build | my-app |
RAILPACK_NX_APP |
Nx app to build and start (project name, package name, or path) | web or @org/web |
RAILPACK_NODE_PLAYWRIGHT_INSTALL |
Install Playwright browsers | 1 |
Playwright
Section titled “Playwright”When Playwright is a production dependency, Railpack suggests setting
RAILPACK_NODE_PLAYWRIGHT_INSTALL=1. Browser installation is opt-in because
it increases image size and is not required by every application that includes
Playwright.
When enabled, Railpack runs Playwright through the detected package manager to install its browser binaries and adds the required runtime system packages. Ensure Playwright is included in your production dependencies so its CLI is available during the build.
Package Managers
Section titled “Package Managers”Railpack detects your package manager in the following order:
- packageManager field: Reads the
packageManagerfield frompackage.json - Mise idiomatic version files: If Mise resolves exactly one of
pnpm, Yarn, Bun, or npm from
package.json(devEngines.packageManagerorpackageManager), that manager is used. A tool listed only inmise.tomlor.tool-versionsdoes not select the package manager. - Lock files: Falls back to detecting based on lock files:
pnpm-lock.yamlfor pnpmbun.lockborbun.lockfor Bun.yarnrc.ymlor.yarnrc.yamlfor Yarn Berry (2+)yarn.lockfor Yarn 1
- engines field: As a fallback, checks the
enginesfield inpackage.jsonfor package manager versions:engines.pnpmfor pnpm versionengines.bunfor Bun versionengines.yarnfor Yarn version- Defaults to npm if no package manager is detected
When the packageManager field selects npm or Yarn, Corepack installs its
specified version.
Railpack supports building native modules and automatically configures node-gyp.
Monorepo Support
Section titled “Monorepo Support”Railpack automatically supports monorepo (workspaces) configurations with all major package managers. No special configuration is required.
Supported Approaches:
- npm, bun, yarn: Uses the
workspacesfield inpackage.json - pnpm: Uses
pnpm-workspace.yamlconfiguration - Nx: Detects
nx.jsonand builds Next.js apps even when targets are inferred (no rootbuild/startscripts)
See the examples
folder in the
repository for workspace examples across different package managers (e.g.,
node-pnpm-workspaces, node-npm-workspaces, node-yarn-workspaces,
node-bun-workspaces, node-nx-next).
When building a monorepo, Railpack will:
- Detect workspace configurations automatically
- Install all workspace dependencies correctly
- Respect workspace dependency links between packages
- Cache workspace node_modules appropriately
If your monorepo requires building a specific workspace package, ensure
your build and start scripts are defined in the root package.json, set
RAILPACK_NX_APP for multi-app Nx workspaces, or use a
configuration file to specify custom commands.
Stock Nx workspaces often rely on inferred
tasks instead of package.json
scripts. When Railpack detects Nx and the root has no build/start scripts:
- Build:
nx build <project>(uses the package name, e.g.@org/web) - Start (Next.js):
cd apps/web && next startso runtime does not depend on thenxCLI
With a single Next.js app, selection is automatic. With multiple apps, set
RAILPACK_NX_APP to the project name, package name, or package path (e.g.
web, @org/web, or apps/web).
TanStack Start
Section titled “TanStack Start”TanStack Start is detected via @tanstack/react-start and is not treated as a
Vite SPA. If there is no start script, Railpack installs srvx globally and
starts with srvx --prod -s ../client dist/server/server.js. For production
Node deploys, set up Nitro per the
TanStack hosting docs.
Railpack caches node_modules/.vite.
Install
Section titled “Install”Railpack will only include the necessary files to install dependencies in order
to improve cache hit rates. This includes the package.json and relevant lock
files, but there are also a few additional framework specific files that are
included if they exist in your app. This behavior is disabled if a preinstall
or postinstall script is detected in the package.json file.
You can include additional files or directories to include by setting the
RAILPACK_NODE_INSTALL_PATTERNS environment variable. This should be a space
separated list of patterns to include. Patterns will automatically be prefixed
with **/ to match nested files and directories.
Static Sites
Section titled “Static Sites”Railpack can serve a statically built Node project with zero config. You can disable this behavior by either:
- Setting the
RAILPACK_NO_SPA=1environment variable - Setting a custom start command
These frameworks are supported:
- Vite: Detected if
vite.config.jsorvite.config.tsexists, or if the build script containsvite build - Astro: Detected if
astro.config.jsexists and the output is not type"server" - Next.js: Detected if
nextis in dependencies andnext.config.js,next.config.mjs, ornext.config.tssetsoutput: 'export'(oroutput: "export"). The defaultnext startstart script does not disable SPA mode. - CRA: Detected if
react-scriptsis in dependencies and build script containsreact-scripts build - Angular: Detected if
angular.jsonexists - React Router: Detected if
react-router.config.jsorreact-router.config.tsexists, or if the build script containsreact-router build. To enable SPA mode, setssr: falsein your React Router config. - Expo Web: Detected if
expoandreact-native-webare in dependencies andapp.jsonsetsexpo.web.outputtostaticorsingle
For all frameworks, Railpack will try to detect the output directory and will
default to dist (or build/client/ for React Router, or out for Next.js
static exports). Next.js reads distDir from your config when set. Set the
RAILPACK_SPA_OUTPUT_DIR environment variable to specify a custom output
directory. Railpack uses your app’s build script to produce the static
output.
Note that if a SPA framework is not detected automatically, can you force SPA mode
by specifying a RAILPACK_SPA_OUTPUT_DIR environment variable. This will enable SPA
mode and serve the specified directory as a static site. Some of the SPA detection
uses regexes on framework configuration files, which will fail if the default framework
configuration files are customized.
Static sites are served using the Caddy web server and a default Caddyfile. You can overwrite this file with your own Caddyfile at the root of your project.
Node SPA deploys honor the index_fallback key in a Staticfile at the
project root when set. SPA routing behavior is unchanged unless you set
index_fallback: false (for example on multi-page Astro static sites) so
unknown paths return 404 and serve 404.html when present.
Framework Support
Section titled “Framework Support”Railpack detects and configures caches and commands for popular frameworks. Including:
- Next.js: Caches
.next/cachefor each Next.js app in the workspace - Remix: Caches
.cache - Vite: Caches
node_modules/.vite - Astro: Caches
node_modules/.astro - React Router: Caches
.react-router - Nuxt:
- Start command defaults to
node .output/server/index.mjs - Caches
node_modules/.cache
- Start command defaults to
As well as a default cache for node modules:
- Node modules: Caches
node_modules/.cache(with the cache keynode-modules)
Cache & Removing node_modules
Section titled “Cache & Removing node_modules”When you add custom build commands that remove node_modules (such as
npm ci), Railpack automatically detects this and
removes the node_modules/.cache directory from the cache configuration for
those steps. This prevents EBUSY: resource busy or locked errors
that would otherwise occur when trying to remove a cached directory.
This automatic handling applies to build steps that contain commands like:
npm cirm -rf node_modulesrimraf node_modules
The install step always retains its cache configuration regardless of the commands used.
System Dependencies
Section titled “System Dependencies”Railpack automatically installs system dependencies for Puppeteer:
- Puppeteer: When detected in workspace dependencies, Railpack installs
all necessary system packages for running headless Chrome, including
xvfb,chromiumdependencies, and font libraries. Note that Puppeteer’s bundled Chromium does not support ARM64; if you need to run on ARM hardware, consider switching to Playwright or implementing a custom workaround (e.g. installing a system Chromium and pointingexecutablePathat it).