Skip to content

nebula.toml reference

Every key of nebula.toml. How the file is used (validate, plan, apply, git, drift) is in the nebula.toml guide; the machine-readable form is the JSON Schema at /schema/nebula.toml.json, which carries the same descriptions and examples.

Conventions#

  • TOML, UTF-8, schema = 1. Unknown keys, wrong types and TOML datetimes are errors.
  • snake_case everywhere. Units live in the key name: _seconds, _mb, _days, _minutes, _percent. No "30s" or "512mb" strings.
  • Services are a map keyed by slug ([services.web]). Collections are maps keyed by their identity: variable name, hostname, volume name, task name, backup key, file path. Arrays are for ordered or atomic lists only (argv, upstreams, CIDRs, watch paths, redirects).
  • name means a name resolved in the workspace (server, git connection, registry, backup destination, network, certificate). An unknown or ambiguous name is a problem, never a guess.
  • staged keys take effect on the next deploy and show in the staged bar. immediate keys take effect at once, like the dashboard control they map to.
  • An absent key is not owned and not changed on an existing service, whatever its default says. The default applies when the service is created.

Root#

Key Type Notes
schema integer Required. Only 1.
"$schema" string Ignored; for editors.
project string Optional project slug; must match the target project.
server string (name) Default server. Precedence: service > environment > root.
variables table Project-layer shared variables, read as ${{shared.KEY}}.
secrets table { keys, scope } Shared secret names, no values.
services table One table per service, keyed by slug.
environments table Optional overrides per environment slug.

auto_apply, is_production, tokens and credentials are not keys.

Services#

Exactly one source key decides the kind:

Source key Kind
image An image service (app, or worker without a port).
git A service built from a repository.
database A managed database.
route An external proxy route; no container.
stack A compose stack.

Common keys: name (immediate), server (staged; required for route), only_in (environments the service exists in), depends_on (deploy order, staged), networks (attach existing shared networks, staged), unset (environment blocks only).

Section image, git database route stack
port, command, health_check, one_shot yes derived no no
stop_grace_seconds, memory_mb, cpus, image_retention, block_egress, docker yes yes no no
pre_deploy, post_deploy, files, volumes, scale, tasks yes no no no
variables, secrets yes yes no yes
domains yes (needs a port) no yes (http routes) yes (component)
backups no yes no no
uptime yes yes yes yes

A key not allowed for the kind is not_allowed_for_kind.

Runtime keys#

Key Type Default Bounds
image string required for image repo[:tag][@digest], no registry credentials
port integer 0 0-65535; 0 means no HTTP
command argv image default [] clears
one_shot bool false Runs to completion on every deploy; excludes pre_deploy and post_deploy
stop_grace_seconds integer 30 1-3600
memory_mb integer 0 (no limit) 0 or at least 6
cpus float 0 (no limit) at least 0; the only float in the format
image_retention integer 5 1-50
block_egress bool false (true for a new database) No outbound internet
generate_domain bool false Attach the platform-generated hostname when the service has none

health_check: type (http, tcp, docker, exec, none), path (http, starts with /), port, interval_seconds (2), timeout_seconds (3), start_period_seconds (0), deadline_seconds (120, 1-3600), success_threshold (2), command (exec; first element CMD or CMD-SHELL). Each key is owned on its own.

pre_deploy and post_deploy: command (argv, required) and timeout_seconds (600, 1-3600).

git and build#

Key Type Notes
git.url string Public repository, or one whose token or deploy key is already stored. Userinfo in the URL is rejected. Exclusive with connection and repo.
git.connection string (name) Git connection; credentials stay in the connection. immediate
git.repo string owner/name. immediate
git.branch string Required.
git.submodules auto, on, off Default auto.
git.auto_deploy bool Default true on a new link; needs connection. immediate
git.watch_paths array, at most 50 Replaces the list; globs and ! exclusions. immediate
build.type auto, dockerfile, railpack, starter:<node|python|go|ruby|php|static> Default auto.
build.dockerfile, build.context relative path Defaults Dockerfile and .. No absolute paths, no ...
build.location inherit, target, build_server Default inherit.
build.server, build.registry string (name) Optional.

Variables and secrets#

A variable value is a string in the variable grammar: a plain value, "${{PORT}}", "${{db.DATABASE_URL}}", "${{shared.KEY}}", "${{team.KEY}}", "${{vault:integration/path#key}}", a generator ("${{secret(32)}}", randomInt, uuid, randomBytes), or a transform ("${{urlencode(db.PASSWORD)}}"). "$${{" writes a literal ${{. Non-string values are wrong_type. Names match ^[A-Za-z_][A-Za-z0-9_]{0,254}$; PORT and NEBULA_* are reserved.

The table form carries metadata: KEY = { value = "...", scope = "build", comment = "...", rotate_every_days = 90, public = false }. scope is runtime (default), build or both; public = true asserts a literal is not a credential. secrets = { keys = ["NAME"], scope = "runtime" } declares names that are secret, with no value in the file. A variable containing a generator is a secret; secret = false with one is rejected.

domains#

[services.<s>.domains."<host>[/<path-prefix>]"]; a table may be empty. Keys (all staged unless noted): target_port, component (stack, required there), strip_prefix, add_prefix, kind (serve, redirect), redirect_to, redirect_code (301, 302, 307, 308; default 308), preserve_path, tls (auto, custom, none), certificate (name), dns_resolver, www_redirect, options (the whole table replaces the live option set), and cloudflare (manage, proxied, origin_lock; immediate, needs a Cloudflare connection).

options: force_https, compress, noindex, security_headers (basic, strict), request_headers, response_headers, cors (origins, methods, headers, expose_headers, credentials, max_age_seconds), redirects (array of regex, replacement, permanent), rate_limit (average, period_seconds, burst, trusted_proxy_depth), access (allow_cidrs, deny_cidrs, trusted_proxy_depth, reject_status), basic_auth (enables only; users and passwords are never in the file), large_uploads.

database and backups#

database is create-only: engine (required), version, flavor, name, username, accept_license (required true for source-available engines). A difference on an existing database is immutable_field.

backups.<key> (key matches ^[a-z0-9][a-z0-9_-]{0,62}$; immediate): destination (name, required), cron (required; at most one run per 5 minutes), timezone (UTC), enabled (true), retention_count (7, 0-1000), retention_days (0, 0-3650). Schedules created in the dashboard have no key and are never touched.

route#

protocol (http, tcp, udp; create-only), upstreams (1-20 inline tables, each { address = "host:port" } or { service = "<slug>", port = 1234 }), scheme, insecure_skip_verify, server_name, pass_host, health_path, verify_skip, listen_port (1024-65535, tcp and udp), allow_cidrs, deny_cidrs, open_to_internet (required when allow_cidrs is empty on tcp and udp), proxy_protocol (0, 1, 2). Needs server. The public port of a database is not a file key.

stack#

Exactly one of compose (up to 256 KiB) and compose_file (relative path, sent as an attachment); wait_seconds (300, 1-3600); resources.<component> with memory_mb and cpus.

volumes, files, scale, uptime, tasks, docker#

  • volumes: name = "/mount/path"; names match ^[a-z0-9][a-z0-9_.-]{0,62}$, paths are absolute. Staged. Detaching never deletes data. A service with volumes cannot have scale.replicas above 1.
  • files."/path": mode (octal string, 0644) and exactly one of content or source (an attachment). At most 16 files, 64 KiB each, 256 KiB in all. The whole set is one unit.
  • scale (immediate): replicas (1-10), autoscaling.enabled, autoscaling.min, autoscaling.max, autoscaling.target_cpu_percent (20-95).
  • uptime (immediate, admin scope): enabled, alerts, failure_threshold (3, 1-20), recovery_threshold (2, 1-20), reminder_minutes (0, 0-1440).
  • tasks.<name> (immediate, at most 20): schedule (required cron), timezone (UTC), command (argv, required), enabled, timeout_seconds (1800, 1-86400), concurrency (forbid, allow).
  • docker: staged, each key owned individually. Allowed everywhere: cap_add, cap_drop, no_new_privileges, read_only_rootfs, pids_limit, hostname, dns, sysctls, host_sysctls, user, group_add, ulimits, shm_size_mb, extra_hosts, labels, init, stop_signal, tmpfs, entrypoint. Only on unrestricted servers: devices, gpus, runtime, privileged, pid_host, seccomp_unconfined, apparmor_unconfined and the capabilities NET_ADMIN and SYS_ADMIN.

Environments#

[environments.<slug>] overrides one environment: server, variables and secrets (the environment layer of shared variables), and services.<slug> with the same keys as a service plus unset. The file never creates, renames or deletes environments.

The merge rule#

For service S in environment E the resolved definition is merge(services.S, environments.E.services.S):

  1. Tables merge by key, recursively.
  2. Scalars and arrays replace the lower value as a whole. An override restates a full list.
  3. Leaves replace as a unit: a variable entry, domains.<host>.options, files.<path>, a route.upstreams item, resources.<component>.
  4. unset (environment blocks only) removes TOML key paths after the merge, for example ["variables.FEATURE_X", 'domains."shop.acme.com"', "tasks.digest", "scale"]. A removed key is not owned in that environment; nothing live is deleted.
  5. only_in filters services before merging. A service defined only in an environment block must be complete.
  6. Error positions refer to the line that supplied the winning value.

Limits#

File 256 KiB; attachments at most 8, 256 KiB each, 1 MiB in all; 64 services; 500 variables per service and per shared layer; 50 domains per service; 16 files per service; 20 tasks per service; 50 watch paths; 20 upstreams; 32 environment blocks; table nesting depth 10; any string 64 KiB.

Problems#

Each problem is { severity, code, message, path, line, column, hint, suggestions, environment }, shown as nebula.toml:19:1: error[unknown_key] services.web.healthcheck: unknown key "healthcheck"; did you mean "health_check"?. The codes and what to do about each are in the troubleshooting table of the guide.

Examples#

A web app from git, a worker, Postgres with nightly backups, a cron task, a proxied domain, an external route, an uptime monitor and staging overrides:

toml
#:schema https://nebula.example.com/schema/nebula.toml.json
schema  = 1
project = "acme-shop"          # must equal the target project's slug when --project / the binding names one
server  = "fra-1"              # default server (by name) for every service; override per service or environment

# Project-wide shared variables. Services read them as ${{shared.APP_NAME}}.
[variables]
APP_NAME = "Acme Shop"

# ───────────────────────── web: built from git ─────────────────────────
[services.web]
name               = "Web"
port               = 3000                 # no port => the service is a worker (no HTTP health check, no domain)
stop_grace_seconds = 30
depends_on         = ["db"]               # deploy order; references in variables add edges automatically

[services.web.git]
connection  = "github-acme"               # git connection NAME (Team settings → Integrations); never a token
repo        = "acme/shop"
branch      = "main"
auto_deploy = true                        # immediate
watch_paths = ["apps/web/**", "packages/**", "!**/*.md"]   # monorepo filter; replaces the whole list

[services.web.build]
type       = "dockerfile"                 # auto | dockerfile | railpack | starter:<node|python|go|ruby|php|static>
dockerfile = "apps/web/Dockerfile"
context    = "."

[services.web.health_check]
type             = "http"
path             = "/healthz"
deadline_seconds = 90

[services.web.pre_deploy]                 # runs before the new release starts; non-zero exit fails the deploy
command         = ["bun", "run", "db:migrate"]
timeout_seconds = 300

[services.web.variables]
NODE_ENV     = "production"
APP_URL      = "https://shop.acme.com"
DATABASE_URL = "${{db.DATABASE_URL}}"     # reference to another service's exported variable
SESSION_SECRET = "${{secret(48)}}"        # generator: runs once, same text again keeps the value
SITE_NAME    = "${{shared.APP_NAME}}"
BUILD_FLAG   = { value = "1", scope = "build", comment = "read by the bundler" }   # table form for metadata

[services.web.secrets]                    # names only; the values are set in the dashboard / CLI / secret manager
keys = ["STRIPE_SECRET_KEY"]

[services.web.domains."shop.acme.com"]    # key = hostname, or hostname/path-prefix
www_redirect = true

[services.web.domains."shop.acme.com".cloudflare]   # immediate; needs a Cloudflare connection
proxied = true

[services.web.domains."shop.acme.com".options]      # replaces the domain's whole option set
compress         = true
security_headers = "strict"

[services.web.scale]                      # immediate
replicas = 2

[services.web.uptime]                     # immediate
failure_threshold = 3
reminder_minutes  = 60

[services.web.tasks.digest]               # cron task, immediate; runs in the service's image
schedule        = "0 7 * * 1-5"
timezone        = "Europe/Berlin"
command         = ["bun", "run", "jobs:digest"]
timeout_seconds = 900

# ───────────────────────── worker: same repo, no port ─────────────────────────
[services.worker]
command    = ["bun", "run", "worker"]
memory_mb  = 512
depends_on = ["db"]

[services.worker.git]
connection  = "github-acme"
repo        = "acme/shop"
branch      = "main"
watch_paths = ["apps/worker/**", "packages/**"]

[services.worker.build]
type       = "dockerfile"
dockerfile = "apps/worker/Dockerfile"
context    = "."

[services.worker.variables]
DATABASE_URL      = "${{db.DATABASE_URL}}"
QUEUE_CONCURRENCY = "8"

# ───────────────────────── db: managed Postgres with nightly backups ─────────────────────────
[services.db.database]                    # create-only keys: engine, version, name, username
engine   = "postgres"
version  = "17"
name     = "shop"
username = "shop"

[services.db.backups.nightly]             # immediate; key = stable identity of the schedule
destination     = "offsite-s3"            # backup destination NAME
cron            = "0 2 * * *"
timezone        = "Europe/Berlin"
retention_count = 14
retention_days  = 30

[services.db.uptime]
reminder_minutes = 30

# ───────────────────────── printer: external route (no container) ─────────────────────────
[services.printer]
name    = "Office printer"
only_in = ["production"]                  # this service exists in the file only for these environments

[services.printer.route]
protocol    = "http"
upstreams   = [{ address = "192.168.1.20:631" }]     # or { service = "web", port = 3000 } for a managed service
health_path = "/"

[services.printer.domains."print.acme.com".options.access]
allow_cidrs = ["203.0.113.0/24"]

# ───────────────────────── staging overrides ─────────────────────────
[environments.staging]
server = "fra-staging"

[environments.staging.variables]          # environment-layer shared variables (override the project layer)
APP_NAME = "Acme Shop (staging)"

[environments.staging.services.web]
unset = ['domains."shop.acme.com"', "scale", "uptime"]   # TOML key paths; only allowed in environment blocks

[environments.staging.services.web.git]
branch = "develop"

[environments.staging.services.web.variables]
NODE_ENV = "staging"
APP_URL  = "https://shop.staging.acme.com"

[environments.staging.services.web.domains."shop.staging.acme.com".cloudflare]
proxied = false

[environments.staging.services.worker.git]
branch = "develop"

[environments.staging.services.db]
unset = ["backups.nightly"]

A single image:

toml
#: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 = 2

A compose stack:

toml
#:schema https://nebula.example.com/schema/nebula.toml.json
schema = 1
server = "fra-1"

[services.shop]
name = "Shop stack"

[services.shop.stack]
wait_seconds = 300
compose = '''
services:
  web:
    image: nginx:1.27-alpine
    environment:
      GREETING: ${GREETING}
  cache:
    image: valkey/valkey:8
'''

[services.shop.stack.resources.cache]
memory_mb = 256

[services.shop.variables]
GREETING = "hello"

[services.shop.domains."shop.acme.com"]
component   = "web"
target_port = 80