Reference

Manifest Reference

stack.toml lives in your project root and is meant to be committed — it's the whole "share a stack" file, the non-container equivalent of a Dockerfile. Every field below is optional unless marked required; a bare stack.toml with just a [project] section is valid.

[project]

FieldTypeDefaultNotes
namestringfolder nameUsed to key stack status/stack logs/stack down and as the default domain prefix.
domainstring{name}.localhost.localhost resolves with zero setup in Chrome/Firefox/Edge — no hosts-file edit. Non-browser clients (curl, backend-to-backend calls) fall back to 127.0.0.1:<port>.
env_filesarray of strings[]Extra .env paths, relative to the project root, loaded in addition to a root-level .env (if present) by stack up, stack doctor --project, and stack load-env when it's called with no explicit path. For a project whose real .env lives somewhere other than the project root.
stack.toml
[project]
name = "acme-api"
domain = "acme-api.localhost"
env_files = ["backend/.env"]   # optional — root .env still loads too, if present

[[clone]]

Array of tables. Lets a bare stack.toml — the only file someone needs to bootstrap your whole project — declare which git repos to fetch. Only runs when stack up is given --clone; plain stack up never touches git, so cloning is something you opt into and can review, not a side effect that happens silently on every run.

FieldTypeDefaultNotes
repostringrequiredAnything git clone accepts (SSH or HTTPS).
pathstringderived from repoWhere it's cloned to, relative to the project root. Unset, it's git's own default naming — the repo URL's last path segment with .git stripped, so git@github.com:acme/api.git lands in ./api. Set it to rename the folder, or to "." to clone directly into the project root (see below).
refstringdefault branchBranch, tag, or commit SHA — cloned, then checked out separately (checkout accepts all three; --branch alone doesn't).
stack.toml
[[clone]]
repo = "git@github.com:acme/api.git"   # -> ./api, git's own default naming

[[clone]]
repo = "git@github.com:acme/shared-lib.git"
path = "vendor/shared-lib"              # explicit override

Cloning into the project root

Someone shares just a stack.toml with path = ".". They drop it in an empty folder and run stack up --clone — but that folder already has stack.toml in it, so it isn't actually empty, and git clone refuses to clone into a non-empty directory. stack handles this case specifically: instead of git clone, it runs git init + git remote add + git fetch + git checkout in place. If the fetched repo also tracks its own stack.toml, checkout fails loudly on the real conflict rather than silently picking a winner — that's the correct outcome, not a bug to work around.

Running --clone more than once

Safe. An entry with an explicit or derived path is skipped once that directory exists. The path = "." case checks for a .git folder instead (since the project directory itself always exists, trivially) — already-initialized is left untouched either way.

[language.<name>]

Either a bare version string, or a table when you need more control — both forms can coexist as siblings, same as Cargo.toml's [dependencies].

stack.toml
[language]
php = "8.3.1"       # simple form
node = "20.11.0"

[language.rust]      # detailed form
version = "1.75.0"
manager = "vfox"
FieldTypeDefaultNotes
versionstringrequired unless path set 
managerstringinferredphp/node → vfox, python → uv. Any other name needs manager set explicitly.
pluginstringthe table keyvfox plugin name override, for the rare mismatch (e.g. Node's plugin is named nodejs).
binarystring{name}.exeBinary filename override.
pathstringBYO — a fixed, already-resolved binary. Bypasses vfox/uv entirely.
workersnumber4PHP only. Sets PHP_FCGI_CHILDREN for stack's default FastCGI execution (see below) — how many concurrent php-cgi worker processes handle requests for this project. Raise it for a project doing real concurrent local load-testing; the default of 4 is plenty for normal day-to-day browsing.
venvstringPython only, in practice. A virtualenv directory, relative to the project root — its Scripts (Windows) folder is added to PATH during activation, right alongside the interpreter itself. Replaces a manual .venv\Scripts\activate step: every console-script your venv installed (pytest, black, a Django manage.py-adjacent tool, ...) just resolves once you cd in, the same as everything else stack manages.

Every project pinning the same language+version shares the one binary from vfox's/uv's own store — nothing is copied per project. Activation (see Everyday use) puts it first on PATH the moment you cd in.

[service.<name>]

FieldTypeDefaultNotes
versionstringrequiredAlso the key every other project shares an instance by — same engine+version reuses the one running process.
schemastringproject nameProject isolation is by schema/database name, not a separate data directory per project.
portnumber or "{VAR}"engine default3306/5432/27017 for mysql/postgres/mongo; required explicitly for any other engine name. A quoted "{VAR}" resolves from the environment (or an interactive prompt with --prompt) the same way command placeholders do — see below.
commandstringengine defaultBuilt in for mysql/postgres/mongo; required for anything else. {port}/{data_dir}/{path} placeholders available.
pathstringBYO binary, this project only. Omit to resolve by version via stack register instead.
externalboolfalseAdopt an already-running instance — stack only verifies it's listening, never starts/stops it. Errors if path/command are also set, or if nothing's actually listening.
stack.toml
[service.mysql]
version = "8.0.35"
schema = "acme_api"   # optional — defaults to the project name

[run]

The one process that needs a stable, routed domain — usually your app's own dev server. Omit [run] entirely and stack still activates languages and starts declared services; it just prints that nothing is routed.

FieldTypeDefaultNotes
commandstringrequired unless external, or [language.php] is declared{port} is substituted; PORT is also set as an env var, for anything reading process.env.PORT. See below if omitted with PHP declared.
portnumber or "{VAR}"ephemeralPin it if anything (OAuth callback, CORS allowlist, a proxy target) hardcodes the port — stack errors clearly rather than silently picking a different one when it's taken. A quoted "{VAR}" resolves like a command placeholder — see below.
cwdstringproject rootWhich cloned path the command runs in — matters once [[clone]] has more than one entry.
externalboolfalseRun the dev server yourself, in your own terminal — stack only validates the port and routes to it, never spawns or tracks the process. port becomes required. See below.

Omitting command for PHP

When [language.php] is declared and command is left out, stack defaults to its own FastCGI execution: php-cgi.exe with real concurrent worker processes, fronted by a Caddy FastCGI route, doc-root auto-detected (public/ if it has an index.php, otherwise the project root). This is the same "sensible default when omitted" pattern [service.*] already has for mysql/postgres/mongo, just for [run]. Set command explicitly (e.g. php -S 127.0.0.1:{port} -t public) if you need something other than FastCGI, or if the install is non-standard and doesn't ship php-cgi.exe next to php.exe.

Why [run].external exists

Sometimes you'd rather run the dev server yourself — watch its own output directly in your own terminal, keep whatever reload workflow you already use, or just not hand the process over to another supervisor. external = true is built for that: stack's only job becomes routing a real domain to the port you tell it about.

stack.toml
[run]
external = true
port = 8000

Why the first stack up on a project asks you to confirm

command here (and every [service.*].command) is a literal program invocation — stack runs it exactly as written. The first time a project's commands are seen, or the first time they change, stack up prints them and asks you to confirm before anything actually executes; declining aborts cleanly with nothing spawned. Approval is remembered in ~/.stack/trust.json, so unchanged commands never prompt again on later runs. Pass --yes to approve without the interactive prompt, for CI or scripted use. Full detail in the CLI Reference's stack up entry.

[tool.<name>]

FieldTypeDefaultNotes
pathstringBYO — any tool you've already installed. Its directory is injected into the [run] process's PATH.
versionstringFor the small set of tools stack knows how to fetch itself (Composer today), or resolved via stack register for anything you've registered globally.

[script.<name>]

Named, project-scoped commands — migrations, tests, seed scripts — run via stack run <name> in the foreground, not started/stopped like [run]/[service.*]. Gets the same resolved language PATH (and venv, if declared) as stack up, so a step can just call python/pytest/etc. directly.

FieldTypeDefaultNotes
commandstring or array of stringsrequiredA single command, a TOML array of steps, or a multi-line string (one step per line) — run in order, stopping at the first step that exits non-zero, with that step's exit code. stack run <name> -- extra args forwards trailing arguments to the last step only.
cwdstringproject rootSame meaning as [run].cwd.
stack.toml
[script.migrate]
command = "python manage.py migrate"   # single step

[script.reset-db]
command = [                              # multiple steps, array form
  "python manage.py flush --no-input",
  "python manage.py migrate",
  "python manage.py loaddata fixtures/dev.json",
]

Like [run]/[service.*].command, a script's commands go through trust-on-first-use before they can execute — checked lazily, only when that specific script is actually run via stack run, not bundled into stack up's check. See the CLI Reference's stack run entry.

Placeholders

Any {VAR_NAME} inside a command string resolves in order: reserved keywords ({port}, {data_dir}, {path}) first, then the process environment (including a project's own .env, loaded automatically before resolution), then — only with stack up --prompt — an interactive prompt, masked for anything that looks like a secret. Nothing left unresolved fails silently: every unresolved name is reported together, not one at a time.

A port field can be a quoted placeholder too — e.g. port = "{MEILISEARCH_PORT}" — resolved the same way (environment, then prompt) and parsed as a number afterward. Reserved keywords aren't available here, since port is itself the thing being resolved; an unquoted number always skips resolution entirely.

Putting it together

A single manifest combining most of the above — two languages, one managed service, one external service, a clone entry, and a custom [run] command:

stack.toml
[project]
name = "acme-api"
domain = "acme-api.localhost"

[[clone]]
repo = "git@github.com:acme/acme-api.git"   # -> ./acme-api

[language]
node = "20.11.0"   # simple form: no other options needed

[language.php]      # detailed form: php needs workers set, so it gets its own table
version = "8.3.1"
workers = 8          # raised from the default 4 for a local load test

[service.mysql]
version = "8.0.35"
schema = "acme_api"

[service.redis]
version = "7.2"
external = true
port = 6379

[run]
command = "php -S 127.0.0.1:{port} -t public"
port = 8000
cwd = "acme-api"

A language can use the simple string form or the detailed table form, never both for the same name — [language]'s php = "8.3.1" and a separate [language.php] table would collide on the same TOML key and fail to parse. Once you need any detailed-form field (here, workers), version moves into that same table.