Skip to content
NebulaCtrldocs

Build detection

The builder decision for a Git build, the files detection reads for each language, and what a Railpack-built release gets by default.

When a service builds from Git and has no Dockerfile at its configured path, NebulaCtrl reads a few marker files from the repository at the commit it builds. If it recognizes the project, it builds with Railpack. To build a service this way, follow Build without a Dockerfile.

Which builder a build uses

The builder is decided in this order. Each layer overrides the ones below only when it sets a value.

  1. [build] builder in nebula.toml, read at the commit being built.
  2. The service's Build engine setting: Detect automatically (the default), Dockerfile or Railpack.
  3. Whether a Dockerfile exists at the configured path. With one, the build uses the Dockerfile. With none, detection decides.
SettingDockerfile existsNo Dockerfile
Detect automaticallyDockerfileRailpack, if the project is recognized; otherwise the build fails
DockerfileDockerfileThe build fails
RailpackRailpackRailpack

An explicit Railpack setting does not need detection to succeed: Railpack runs even when detection recognizes nothing. When the control plane can read the repository, an explicit Dockerfile setting fails with a reason that names the missing path.

Where the decision is made

The control plane decides before the build starts, whenever it can read files from the repository's host:

  • a repository of a connected GitHub, Forgejo or GitLab connection;
  • a public repository on GitHub, GitLab.com or Codeberg;
  • a public repository on a Forgejo or Gitea instance, which the control plane recognizes when the host answers /api/v1/version.

For any other public Git host, the control plane cannot read a file, so the build decides after it clones. If a Dockerfile exists at the configured path, the build uses it with BuildKit. If none exists, Railpack prepares a build plan from the build context. When Railpack cannot, the build fails with no Dockerfile at Dockerfile, and Railpack could not build this repository automatically; add a Dockerfile, or a package.json, go.mod, requirements.txt, Gemfile, composer.json or Cargo.toml. The build log says which builder ran. On these hosts nebula.toml is not read, so its [build] settings do not apply, and the service form shows no detection preview. The service's own Build engine setting still applies: with Dockerfile, BuildKit reports a missing Dockerfile.

What detection reads

Detection reads a bounded set of files at the commit, at most 10 files, and stops at the first rule that resolves. It never lists a directory and never runs Railpack. When the control plane decides, it reads the files at the root of the repository. The preview on the service form, and a build job that decides after cloning, read them from the build context. For a service whose build context is ., these are the same files. For a service with another build context, the control plane's decision reflects the root files, not the context directory's.

The first marker found decides the provider:

Marker fileProvider
package.jsonNode
go.modGo
Cargo.tomlRust
pyproject.tomlPython. A [tool.poetry] table is noted in the log and changes nothing else
requirements.txtPython
GemfileRuby
composer.jsonPHP
index.htmlStatic site, built by Railpack's static file provider
none of theseNot buildable

For every provider except Node, the marker file alone decides the provider. Railpack itself inspects the full checkout when it builds.

When nothing is recognized, the build fails:

no Dockerfile at Dockerfile on main and nothing recognizable to build automatically; add a Dockerfile, or add a package.json (or go.mod/requirements.txt/Gemfile/composer.json/Cargo.toml) for a project NebulaCtrl can detect

A package.json that is not valid JSON counts as not buildable, and the build fails with the message above.

Node projects

For a Node project, detection reads package.json and a lockfile, then resolves these facts:

FactHow it is resolved
Package managerThe first lockfile found, in this order: pnpm-lock.yaml (pnpm), bun.lockb or bun.lock (bun), package-lock.json (npm), yarn.lock (yarn). Without a lockfile: the prefix of package.json#packageManager, then the first of pnpm, yarn and bun that appears in a script, then engines.pnpm, engines.yarn or engines.bun. If nothing matches: npm
Node versionengines.node, recorded as given
FrameworkThe first match among dependencies and devDependencies, in the order below. No match is a generic Node app
Install command<manager> install
Build and start commandsscripts.build and scripts.start, run through the package manager. A missing script is left to Railpack
PortSee below
Static outputAstro only
MonorepoA non-empty workspaces in package.json, or a pnpm-workspace.yaml. Informational: no sub-app is selected

Frameworks are matched in this order: Next.js, Nuxt, SvelteKit, Astro, Remix, TanStack Start, Angular, Gatsby, Vite, Create React App, Vue, NestJS, AdonisJS, Elysia, Hono, Fastify, Koa, Express. Vite does not match when a backend framework from the last group (Express, Fastify, Koa, NestJS, Hono, Elysia, AdonisJS) is also a dependency, so a vite.config.js beside a real backend never hides it. Vue is reached only when Nuxt did not match.

The port comes from the first of scripts.start, scripts.dev, scripts.serve and scripts.preview that sets one with --port N, --port=N, -p N or PORT=N. If no script does, a framework default applies: 3000 for Next.js, Nuxt, TanStack Start and a generic Node app, 4321 for Astro, 5173 for Vite. Every other framework gets no default.

An Astro project is static unless the first of astro.config.mjs, astro.config.ts and astro.config.js that exists contains output: "server", in single or double quotes. The scan is a text search, not an evaluation of the config.

Detection records what it found on the build. The build log shows it as a detect step that lists the package manager, the framework or provider, the Node version and each fact detection used.

What a Railpack release gets

A release made from a Railpack build applies these defaults. A value you set always wins, except where noted.

  • User. Every process without a user runs as user 1000. A run_as_user in nebula.toml, or a user set on the process, replaces it. An explicit 0 is never changed.
  • PORT. When the service has exactly one http process, the release sets PORT to that process's port. A variable named PORT replaces it.
  • HOME. The release sets HOME to /tmp. A variable named HOME replaces it.
  • Pinned port. When detection read the port from a package.json script and the service has exactly one http process, the release uses the script's port for that process, replacing the process's own port when the two differ, because a script that hard-codes its port ignores PORT. The rollout log records the change. A framework default port is never pinned. A service with more than one http process keeps its ports.

Cluster requirements

The cluster's agent must support Railpack builds. A build that needs Railpack on a cluster without that support fails before any job runs, and the message says the cluster's build policy, Build CRD or agent predates Railpack builds. Update the agent as described in Update an agent.

For a repository whose host the control plane cannot read, the build also needs an agent that can decide the builder after the clone. An agent that supports Railpack but not that decision fails the build and names the cluster and the agent version. Set the service's Build engine to Dockerfile or Railpack to build without updating. An agent that predates Railpack builds builds the Dockerfile and notes that in the log.

A Dockerfile build that sets a [build] target, [build.args] or a Build target needs an agent that supports build options. A Railpack build has no Dockerfile stages: target and [build.args] in nebula.toml are refused together with builder = "railpack", and a Build target set on the service is ignored.

On this page