Skip to content
NebulaCtrldocs

nebula.toml project file

Every table and key of the nebula.toml that declares a project's services, databases and connections, with limits, wiring rules and the problems the parser reports.

A project file is a nebula.toml at the root of a project's config repository. It declares the project's services and databases and how they connect, instead of configuring one service. It is told apart from a service file by a top-level [services] or [databases] table. A service file may hold neither. To set one up, follow Describe a project in Git.

Example

The example declares a Postgres database with two read replicas in production, a Valkey cache, an API that connects to the cache, and a web service that connects to the API.

nebula.toml
schema = 1

[databases.db]
engine  = "postgres"
version = "18"
size    = "10Gi"

[databases.db.postgres]
readers = 0

[databases.db.environments.production.postgres]
readers = 2

[databases.cache]
engine = "valkey"

[services.api]
context = "services/api"
connect = ["cache"]

[services.api.env]
DATABASE_URL = "${{ db.DATABASE_URL }}"

[services.api.connection]
API_URL = "http://${{ host(api) }}:8080"

[services.api.deploy]
release_command = "bin/migrate"

[services.api.processes.web]
port = 8080

[services.api.environments.production.processes.web]
replicas = 3

[services.web]
context = "apps/web"
connect = ["api"]

[services.web.processes.web]
port = 3000

[services.web.env]
SESSION_SECRET = "${{ secret(64) }}"

[services.web.secrets]
STRIPE_API_KEY = "Stripe secret key: Dashboard, Developers, API keys"

The wiring:

  • connect = ["cache"] gives api the variable REDIS_URL_CACHE = ${{ cache.REDIS_URL }}.
  • connect = ["api"] gives web the variable API_URL = ${{ api.API_URL }}.
  • DATABASE_URL is a plain reference under the app's own name.
  • readers = 0 is the base, and production overlays it with two read replicas.

Validate a file

nebula config validate takes a project file like a service file, and detects it by its top-level tables.

nebula config validate nebula.toml

Offline, the command checks the shape of every table, every connect target and reference between the resources the file declares, each depends_on entry that names its own service, cycles, engine versions, and each service block resolved for the base, every environment the file overlays, and production, development and preview. A valid file prints one line per resource under the success line:

nebula.toml: ok (2 databases, 2 services)
  databases.cache: valkey, newest version
  databases.db: postgres, 18
  services.api: builds services/api, connects cache
  services.web: builds apps/web, connects api

A problem prints as <file>:<line>: <path>: <message>, with the dotted path from the root of the file. The exit status is 1 when there is any:

nebula.toml:5: services.web.connect: services.web connects to api, which publishes no connection variables; add [services.api.connection] or reference a key with ${{ api.KEY }}

The command cannot know what the project already has. A reference to a slug the file does not declare is not checked offline, and neither is adoption. The control plane checks those when it reads the file.

Top level

schema

Type integer · Required · Constraints must be 1

The schema version. A file without it fails with schema: is required; write schema = 1 at the top of the file.

[services] and [databases]

Type tables of blocks, keyed by slug · Default none · Constraints at least one resource in total; 20 services and databases together

A project file holds only schema, [services] and [databases]. Any other top-level key is a problem. A service-file key at the top level gets a message that says where it goes, such as "build" is a service setting; in a project file write it inside a service, as [services.<slug>.build].

The parser reads the file under the rules of the service file: strict TOML 1.0 in UTF-8, and every problem collected in one pass. The file may be 256 KiB.

Slugs

The table key of a service or database is its slug. Services and databases share one set of slugs. A slug is lowercase letters and digits in single-hyphen-separated segments, starting with a letter, at most 40 characters. inputs is reserved. A slug used by both a service and a database is a problem. Renaming a key declares a new resource, and the old one is handled by the removal rule.

[services.<slug>]

A service block names its source and its connections, and may hold the tables of a service file.

context

Type string · Constraints exactly one of context and image; relative, no .., "." for the root

A git service built from the config repository, from this directory.

image

Type string · Constraints exactly one of context and image; no spaces

An image service. The reference's repository is the service's image, and the reference is what its first deploy runs, such as ghcr.io/acme/api:1.2. The first deploy happens only while the service has no running release in the environment. Later versions deploy as for any image service, from the console or a push trigger, and a change to the tag in the file changes nothing.

A service built from another repository is not declared. It stays managed in the console, and the file may still reference it.

connect

Type array of slugs · Default none · Constraints unique; never the service itself

Services and databases this service takes connection variables from. See Connections.

[services.<slug>.connection]

Type table of string to string · Default none · Constraints keys match ^[A-Z_][A-Z0-9_]*$; at most 100

The variables this service publishes to services that connect to it. Values may use references.

[services.<slug>.secrets]

Type table of string to string · Default none · Constraints keys match ^[A-Z_][A-Z0-9_]*$; at most 100

Variable keys the service needs and Git must not hold. Each value is a description of where to get the secret. See Variables the file owns.

build, deploy, env, processes, mounts, environments

Type the tables of a service file · Default none

These write inside the block, with the same keys, limits and checks: [services.<slug>.build], [services.<slug>.deploy], [services.<slug>.env], [services.<slug>.processes.<name>], [[services.<slug>.mounts]] and [services.<slug>.environments.<slug>]. A [deploy] depends_on names slugs of the file's resources or of the project's.

A service is configured by one file. A service declared here whose own context directory also holds a nebula.toml is a problem: services.api is declared in the project config, and services/api/nebula.toml also configures it; keep one of them.

Services in a build context

A build that finds a project file as the nebula.toml of its build context does not refuse the tables as unknown keys. A service with build context . reads the root file, so this rule applies to it. The build log records what happened in its config step:

  • The file declares the service. The build uses the service's [services.<slug>] block as its file, without [env], and logs using [services.<slug>] of the project config.
  • The file does not declare the service. The build runs with no service file and logs nebula.toml is a project config that does not declare <slug>; building without a service file. For the root file the message begins with the root nebula.toml.
  • The file does not parse. The build fails before any job starts, like an invalid service file.

A declared service whose own directory has no nebula.toml reads the root file. A root file that is a service file never stands in for another service.

[databases.<slug>]

A database block creates the same database service the console creates. Postgres runs on CloudNativePG. The project file never describes a Postgres cluster itself.

engine

Type postgres, valkey, mysql or mongodb · Required

The engine NebulaCtrl runs.

version

Type string · Default the newest · Constraints an engine major that the control plane runs

An engine major such as "18". The control plane runs PostgreSQL 18 and 16, Valkey 9 and 7, MySQL 8, and MongoDB 7. An unknown version fails with postgres 15 is not a version this control plane runs; use one of 18, 16.

size

Type quantity string · Default 10Gi · Constraints greater than zero, at most 1Ti

The initial volume size. It is used only when the database is created. An existing volume keeps its size.

[databases.<slug>.postgres]

Type table with readers · Default none · Constraints allowed only when engine = "postgres"

The only engine-specific table. readers is the CloudNativePG reader count, an integer from 0 to 5. A valkey, mysql or mongodb table has no settings and is refused. A table for another engine than the database's own is refused.

[databases.<slug>.environments.<slug>]

Type table holding the engine table · Default none

Overlays the engine table for one environment, as in [databases.db.environments.production.postgres]. size, version and engine cannot be overlaid, because a database has one of each in every environment.

A database is a separate table so that a file can never give a database processes, an image or a command. Point-in-time recovery, backup schedules and promotion stay in the console, because point-in-time recovery names an object store credential.

Wiring

References

Any value in [env] or [connection] may use ${{ slug.KEY }}, ${{ secret(N) }} and ${{ host(slug) }}, which the same scanner reads as in a service variable. ${{ inputs.KEY }} is refused. See Variable expressions.

  • A ${{ slug.KEY }} must name a service or database that the file declares or the project already has, and a key it will have.
  • A reference cycle is a problem: variable references form a cycle: ...; replace one of them with a value.
  • secret(N) draws N random letters and digits. N is 16 to 128 and defaults to 32.

Connections

connect = ["slug"] writes the target's connection variables into the consumer's service tier, each valued as a reference to the target. A database supplies its engine's variable, as connecting it in the console does. For PostgreSQL it also supplies the read endpoint.

EngineVariables written, with <SLUG> the slug in capitals and hyphens as underscoresValues
postgresDATABASE_URL_PG_<SLUG>, DATABASE_READ_URL_PG_<SLUG>${{ slug.DATABASE_URL }}, ${{ slug.DATABASE_READ_URL }}
valkeyREDIS_URL_<SLUG>${{ slug.REDIS_URL }}
mysqlDATABASE_URL_MYSQL_<SLUG>${{ slug.MYSQL_URL }}
mongodbDATABASE_URL_MONGO_<SLUG>${{ slug.MONGODB_URL }}

A service supplies the keys of its [connection] table, under the same names, and those keys are also written as the target's own variables so that the references resolve. connect to a service that has no [connection] table is a problem: services.web connects to api, which publishes no connection variables; add [services.api.connection] or reference a key with ${{ api.KEY }}.

A reference or connect also decides the order of the first deploys: what a service reads or connects to deploys before it. See Deploy order.

Variables the file owns

Variables from [env], [connection] and connect are service-tier variables that the file owns while it names them. The console shows them read-only, labeled Project nebula.toml, and they become ordinary variables once the file stops naming them. The API refuses to change one with 409: KEY is set by the project's nebula.toml at 3f9c2ab; change it in the file and push, or remove it from the file to edit it here.

  • A key written twice on one service is a problem, such as by [env] and connect.
  • A plain value over an existing secret that the file does not own is a problem, because a value from Git never replaces a secret. Write ${{ secret(32) }} to have a new one drawn, or keep setting the variable in the console.
  • ${{ secret(N) }} is drawn once per environment, when the key is first written, and sealed. It is not drawn again while the key stays in the file.
  • A [secrets] key is created once, empty and sealed, in each environment that lacks it. It is never the file's: set the value in the console. A key that [env], [connection] or connect also writes is a problem. The Variables tab marks an empty variable, and a deploy never waits for it.

What the file never names

Domains, secret values, the cluster binding, the deployment strategy, production approval rules and point-in-time recovery stay in the console. Domains reach DNS and Cloudflare, so a push does not change them.

Limits

LimitValue
File size256 KiB
Services and databases together20
Slug40 characters
readers0 to 5
sizegreater than zero, at most 1Ti
[connection] and [secrets] keys100 each

The limits of a service file apply inside each service block.

Problems

A problem is {path, line, message}, with dotted paths from the root, such as services.api.env.DATABASE_URL or databases.db.postgres.readers. Besides the shape problems above, nebula config validate refuses a file when:

  • A connect target, reference or host(slug) names a resource that neither the file declares nor, on the control plane, the project has, or a key that it will not have.
  • A depends_on slug names itself or, on the control plane, a resource that does not exist. A list that closes a cycle with other lists, the connect targets and the references is refused: api → worker → api would never finish deploying: remove worker from [services.api.deploy] depends_on, or the entry for api in [services.worker.deploy] depends_on.
  • A database asks for an engine or a version that the control plane does not run.
  • The file declares nothing: the file declares nothing; add a [services.<slug>] or a [databases.<slug>].

The control plane also refuses a file when it meets what the project already has:

  • A service or database exists with the slug but another kind: db is a valkey database here, but nebula.toml declares it as postgres; rename one of them.
  • A database runs another major: db runs postgres 17 here; nebula.toml asks for 18; a database's data needs the version it was created with, so restore a dump into a new database. A file that sets no version never conflicts.
  • A service has another source: api builds from acme/api here, but nebula.toml builds it from services/api of the config repository; change the service's source in its settings, or rename one of them.
  • A plain value would replace a secret: KEY is a secret variable on api that nebula.toml does not own; ....

Only the control plane can find these, because only it knows the project. nebula config validate does not.

On this page