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.
[build] builderinnebula.toml, read at the commit being built.- The service's Build engine setting: Detect automatically (the default), Dockerfile or Railpack.
- Whether a Dockerfile exists at the configured path. With one, the build uses the Dockerfile. With none, detection decides.
| Setting | Dockerfile exists | No Dockerfile |
|---|---|---|
| Detect automatically | Dockerfile | Railpack, if the project is recognized; otherwise the build fails |
| Dockerfile | Dockerfile | The build fails |
| Railpack | Railpack | Railpack |
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 file | Provider |
|---|---|
package.json | Node |
go.mod | Go |
Cargo.toml | Rust |
pyproject.toml | Python. A [tool.poetry] table is noted in the log and changes nothing else |
requirements.txt | Python |
Gemfile | Ruby |
composer.json | PHP |
index.html | Static site, built by Railpack's static file provider |
| none of these | Not 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 detectA 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:
| Fact | How it is resolved |
|---|---|
| Package manager | The 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 version | engines.node, recorded as given |
| Framework | The first match among dependencies and devDependencies, in the order below. No match is a generic Node app |
| Install command | <manager> install |
| Build and start commands | scripts.build and scripts.start, run through the package manager. A missing script is left to Railpack |
| Port | See below |
| Static output | Astro only |
| Monorepo | A 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. Arun_as_userinnebula.toml, or a user set on the process, replaces it. An explicit0is never changed. PORT. When the service has exactly onehttpprocess, the release setsPORTto that process's port. A variable namedPORTreplaces it.HOME. The release setsHOMEto/tmp. A variable namedHOMEreplaces it.- Pinned port. When detection read the port from a
package.jsonscript and the service has exactly onehttpprocess, 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 ignoresPORT. The rollout log records the change. A framework default port is never pinned. A service with more than onehttpprocess 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.
Related
nebula.tomlservice file for[build].- Build without a Dockerfile for the steps.
- Builds for how a build becomes a release.
Compose mapping
How each Docker Compose file, service and key becomes a NebulaCtrl service, database or variable, what blocks an import, and the limits of one import.
Variable expressions
The syntax of ${{ }} expressions in variable values, what each form produces, when it is evaluated, where it is allowed, and the messages a bad one produces.