Applications
View as Markdownnebula.toml: your project as a file
nebula.toml describes a project's services, domains, variables (names and references, never secret values), tasks, uptime monitors, backup schedules, scaling, volumes, files and per-environment overrides in one file you keep in your repository. Everything it can say, the dashboard can say too: applying the file stages the same changes a dashboard edit would, and applies the settings that take effect at once. It never deploys on its own.
Every key is listed in the nebula.toml reference. Editors that read JSON Schema complete and check the file from /schema/nebula.toml.json; put #:schema <your-address>/schema/nebula.toml.json on the first line.
railpack.json is a different file: the builder reads it from your repository to decide how to build the image. nebula.toml decides what runs, where, and with which settings.
A small file#
#:schema https://nebula.example.com/schema/nebula.toml.json
schema = 1
server = "fra-1"
[services.whoami]
image = "traefik/whoami:v1.10"
port = 80
[services.whoami.health_check]
type = "http"
path = "/health"
[services.whoami.variables]
GREETING = "hello"
[services.whoami.domains."whoami.acme.com"]
[services.whoami.scale]
replicas = 2Exactly one source key per service (image, git, database, route or stack) decides its kind. Scalars go before sub-tables in every TOML table.
The lifecycle: validate, plan, apply, deploy#
| Step | Command | What happens |
|---|---|---|
| Validate | nebulactrl config validate |
Reads the file and reports every problem with file:line:col, a stable code and "did you mean" suggestions. --offline checks the file alone. Exit code 1 if anything is wrong. |
| Plan | nebulactrl config plan --env production |
Compares the file with the environment and lists every change, marked staged or applies at once, and why it is there. Read-only. --detailed-exit-code returns 3 when changes are pending. |
| Apply | nebulactrl config apply --env production |
Re-plans and stages the changes in one transaction, then applies the immediate settings. Pass --plan-id to fail if anything changed since the plan. |
| Deploy | nebulactrl config apply --deploy, or the Deploy key |
Staged changes take effect on the next deploy, like any dashboard edit. |
Staged changes are visible in the staged bar, with set by nebula.toml beside the lines the file owns, and you can discard them. Settings that act on a running service (scaling, tasks, backup schedules, uptime, a git link, DNS records) apply at once, like their dashboard controls.
The same calls exist in the API (validateProjectConfig, planProjectConfig, applyProjectConfig, exportProjectConfig) and, for agents, as one MCP tool, sync_config, in the config toolset (?toolsets=core,config): its action is export, plan or apply, and plan includes the validation.
Ownership and drift#
- A key present in the file (after the environment's overrides) is owned by the file. A key you leave out is not touched.
- Removing a key releases it: the live value stays, the file stops managing it. Removing a service from the file never deletes it; the plan lists it as not in file.
--pruneadditionally deletes only the variables, domains, volumes, networks, links, tasks and backup schedules the file owned before and no longer names. It never deletes services, databases or stored backups, and it needs--yes. Agents cannot prune.- Editing an owned field in the dashboard is allowed. It shows as edited here (drift) with a file icon beside the control, and the next apply overwrites it. From the icon you can Revert to file or Stop managing that field.
- The comparison is three-way: what the last apply wrote, the file, and what is live. The plan says whether a change is file changed, edited in dashboard or both changed.
Git integration#
An environment reads its file from a repository: either set under Environment → Config from git, or derived when all its git services share one repository and branch. On every push the control plane reads the file at the pushed commit and marks the environment:
- nebula.toml differs: a banner above the canvas, with Review. The review dialog is the plan; Stage changes applies it without deploying.
- invalid: the banner shows the first error with its line and a link to the file at that commit.
In a monorepo keep one root file declaring all services; each service picks its code with git.watch_paths, build.context and build.dockerfile.
Auto-apply (per environment, off by default, needs the admin scope) stages a plan on push when it has no blockers, no prune and nothing privileged (Docker options that need an unrestricted server, NET_ADMIN, SYS_ADMIN). It never deploys, and it is never available in production. auto_apply is a setting of the control plane, not a key of the file: a repository cannot switch on its own automation.
Production#
Production never auto-applies: it gets the banner and the review. A person applies the plan. An agent working through MCP may stage changes like it does with stage_changes; if the plan has changes that apply at once, the production guard asks a human to approve that exact plan first.
Start from what you have#
nebulactrl config export --env production > nebula.toml (or Export nebula.toml in the environment menu) writes your running project as a file: defaults are left out, secrets appear as names only, and what the format cannot say is listed as a warning. --all factors every environment into a base plus [environments.<slug>] overrides. A fresh export plans to no changes. nebulactrl config init writes a starter file from the repository you are in.
Secrets#
Never write a secret value. Use secrets.keys = ["NAME"] (names only; set the value in the dashboard or with nebulactrl vars set), a generator such as "${{secret(32)}}", or a reference such as "${{db.DATABASE_URL}}" or "${{vault:integration/path#key}}". There are no keys for tokens, passwords, deploy keys or registry credentials. See variables and secrets.
Writing the file with an AI assistant#
- Start from an export of the live project (
sync_configwithaction: "export", ornebula config export), orconfig initfor a new repo. Never write the file from memory. - Always plan before you apply.
sync_configwithaction: "plan"checks the file and shows what it would change (CLI:config validate, thenconfig plan). Read every diagnostic; fix errors by theirpath,lineandsuggestions. Apply only a plan withvalid: true; pass itsplanId. - Never write secret values. Use
secrets.keys = ["NAME"], a generator ("${{secret(32)}}") or a reference ("${{db.DATABASE_URL}}","${{vault:…}}"). Never put tokens, passwords, private keys or credentials in URLs. Never add keys for credentials: there are none. - Use only keys from the schema. Unknown keys are errors. Keys are
snake_case; units are in the key name (memory_mb,timeout_seconds); all numbers are plain integers; variable values are always strings. - One source key per service (
image,git,database,routeorstack); the key decides the kind. Do not setport,commandorimageon adatabase. - Scalars before sub-tables in every TOML table; services and collections are tables keyed by name (
[services.web],[services.web.domains."app.example.com"]), not arrays. - Tables merge by key; scalars and arrays replace. An environment block restates whole arrays. To drop something in one environment use
unset. - Put hostnames, servers and per-environment values inside
[environments.<slug>]when the project has more than one environment. - A key you write is owned by the file and will overwrite dashboard edits; a key you omit is left alone. Do not rewrite fields you were not asked to change. Removing a service from the file does not delete it.
sync_configwithaction: "apply"stages; it never deploys. Review withreview_changes, then deploy withapply_changes. In production, immediate changes need human approval; do not retry around it.- Do not set
auto_apply; it is not a file key and agents cannot enable it.
Troubleshooting by code#
Each problem carries a stable code. Fix the key at path (line and column are in the report).
| Code | Meaning and fix |
|---|---|
toml_syntax |
The file is not valid TOML. Look at the reported line; a scalar written after a [table] header belongs to that table. |
file_too_large, limit_exceeded |
A limit was passed (file 256 KiB, 64 services, 500 variables per service, 50 domains, 20 tasks). Split the project or remove entries. |
invalid_encoding |
The file must be UTF-8 without a BOM or NUL bytes. |
schema_unsupported, missing_required |
schema = 1 is required; so are the keys named in the message. |
unknown_key, wrong_type, invalid_value |
A key not in the schema, a value of the wrong TOML type (variable values are strings), or a value out of bounds. Use the suggestion. |
invalid_slug, reserved_slug |
Service slugs are lower-case letters, digits and -; shared, team, vault and doppler are reserved. |
missing_source, conflicting_source |
A service needs exactly one of image, git, database, route, stack. |
not_allowed_for_kind, derived_field |
The key does not exist for this kind of service, or the platform derives it (a database's image and port). |
unset_in_base, invalid_unset_path |
unset works in environment blocks only, and its paths must exist in the base definition. |
credential_key, credential_in_url, literal_secret, secret_in_file, secret_has_value |
A credential was written into the file. Use secrets.keys, a generator or a reference. |
invalid_reference, unknown_service_reference, unresolved_reference, reference_cycle, invalid_template, reserved_variable |
A ${{…}} expression is malformed, names a missing service, loops, or sets PORT or NEBULA_*. |
path_escape, attachment_missing |
A referenced file (compose_file, files.*.source, a Dockerfile path) leaves the repository or was not sent. |
unknown_environment, project_mismatch |
The environment or project named does not exist, or the file's project differs from the target. |
unknown_server, unknown_connection, unknown_registry, unknown_destination, unknown_network, unknown_certificate, ambiguous_reference |
A name in the file does not match exactly one object in the workspace. |
immutable_field, source_type_change |
A create-only key (database engine, route protocol) or a service's kind cannot change through the file. |
hostname_in_use, invalid_upstream, upstream_denied |
Another service owns the hostname, or the route's upstream is not allowed on that server. |
license_not_accepted |
Set accept_license = true for a source-available database engine. |
docker_unrestricted, auto_apply_privileged |
The option needs an unrestricted server; auto-apply never applies it, a person must. |
cloudflare_not_connected, replicas_require_stateless, egress_blocked |
The matching dashboard prerequisite is missing. |
basic_auth_no_users, secret_missing_value, secret_flag_differs |
Warnings: set the missing value or users in the dashboard; the variable will be marked secret on the next apply. |
plan_stale, generation_conflict |
The file or environment changed since the plan. Plan again. |
insufficient_scope, approval_required |
The token lacks a scope, or production needs a person's approval of this plan. |
production_auto_apply_unsupported |
Auto-apply cannot be turned on in production. |
deferred |
Information: a setting will run once the service is deployed. |