The manifest¶
stackvo.json sits in the project folder and describes the project. It is
the only file you are meant to edit by hand; everything else is rendered
from it. The contract is contracts/project.schema.json in the repository;
this page is that contract in plain words.
Two fields are required: name and domain. Everything else has a default.
Top level¶
| Field | Type | Default | What it says |
|---|---|---|---|
name |
string | — | The project's identifier. Must match the folder name. Lower case, letters, digits, dots, dashes, underscores. |
domain |
string | — | The address the project opens at. Convention: <name>.loc. Needs a hosts line, which the app writes. |
runtime |
php node python go ruby rust bun deno |
php |
What runs the project. Absent means PHP. |
server |
nginx apache caddy frankenphp swoole roadrunner |
nginx |
The web server, for PHP. Ignored for other runtimes — Traefik proxies straight to the app's port. |
document_root |
string | public |
The folder the web server publishes, relative to the project. No leading slash. |
aliases |
list of strings | [] |
Extra hostnames the same project answers on. *.shop.loc is a wildcard: it goes into the certificate and the router, not the hosts file. |
lan_share |
boolean | false |
Also answer on a name other devices on the network can resolve, through sslip.io. |
services |
list of ids | [] |
The backing services the project needs — mysql, redis, mailpit — by catalogue id. The half of the environment a teammate gets by cloning. |
commands |
object | — | Commands the project offers as buttons. See below. |
hooks |
object | {} |
Commands to run after a build, after a start, before a stop. |
schedule |
list | [] |
Named jobs on a timer, each with its own log. |
processes |
object | {} |
Long-running processes under the container's own supervisord — a queue worker, a scheduler. Survive a rebuild because they are rendered from here. |
components |
object | — | Other directories of this repository, each with its own runtime and address. |
sidecars |
object | — | Containers the project needs that the catalogue does not have. They come and go with the project. |
providers |
object | — | Named places the project's data really lives, and how to fetch it or send it back. |
The runtime block¶
One block, named after the runtime. php for PHP, node for Node, and
python, go, ruby, rust, bun or deno for the others. Every field
inside is optional; an absent field takes the ecosystem's default.
| Field | What it says |
|---|---|
version |
5.6 to 8.5. Default 8.4. |
extensions |
Compiled into the image. One that cannot be built on the chosen version is flagged before you save. |
xdebug |
Whether the extension is in the image. Toggled from the Xdebug card. |
"node": {
"version": "22",
"package_manager": "pnpm",
"install": "pnpm install",
"build": "pnpm build",
"start": "pnpm start",
"port": 3000
}
| Field | What it says |
|---|---|
version |
16 to 23. Default 22. |
package_manager |
Enables Corepack, so packageManager in package.json pins a version. |
install, build, start |
The three commands. build may be empty. |
port |
What the app listens on inside the container. It must bind to 0.0.0.0. |
Commands¶
"commands": {
"reindex": {
"exec": ["php", "artisan", "app:reindex"],
"about": "Rebuild the search index",
"interactive": false
}
}
| Field | What it says |
|---|---|
| id (the key) | Lower-case letters, digits and dashes. The button's name. |
exec |
The program and its arguments, as a list. No shell, so nothing is interpreted. |
about |
The one line shown under the button. |
interactive |
Whether it needs a terminal. |
Commands run in the project's container and nowhere else.
Hooks¶
"hooks": {
"post-build": [["composer", "install"]],
"post-start": [["php", "artisan", "migrate", "--force"]],
"pre-stop": []
}
Each is a list of commands, run in the container. There is no pre-start,
on purpose: before a start there is no container to run in.
Schedule¶
"schedule": [
{ "label": "Nightly report", "cron": "0 2 * * *", "exec": ["php", "artisan", "report:nightly"], "enabled": true }
]
Named jobs, each with its own last run and its own log — rather than one all-or-nothing scheduler process.
Processes¶
"processes": {
"scheduler": { "exec": ["php", "artisan", "schedule:work"] },
"queue": {
"exec": ["php", "artisan", "queue:work", "rabbitmq", "--queue=photos,default", "--tries=3"],
"replicas": 2,
"stopWait": 30
}
}
Long-running processes, run by the supervisord that is already in the
project's container beside php-fpm and the web server. Each key becomes a
[program:<id>] block in the generated supervisord.conf.
That file is regenerated on every rebuild, which is the reason this block
exists: a program added inside the container, or dropped into conf.d/, is
gone at the next build. Declared here, a rebuild restores it and a teammate's
clone gets it. The Supervisor pane lists them beside the image's own two, and
when the manifest declares something the running daemon does not have yet, it
offers to apply the config without a rebuild.
| Field | Default | What it says |
|---|---|---|
exec |
— | Program and arguments, as an array. Runs in /var/www/html. No shell. |
enabled |
true |
false keeps the entry in the file and out of the daemon. |
replicas |
1 |
How many copies. Above one they are <id>_00, <id>_01, … in one group. |
stopWait |
10 |
Seconds after SIGTERM before supervisord kills. A queue worker mid-job wants more. |
Only nginx and caddy projects run supervisord; for any other server the
block is kept and does nothing. php-fpm, nginx and caddy are reserved
ids. Each process writes to its own file, /var/log/supervisor-<id>.log,
rotated by supervisord at 10 MB with three kept, stderr folded in; the
Supervisor pane's Log tab reads it.
Not to be confused with schedule, which starts a command on a timer and
expects it to exit, or with the Workers pane, which runs Laravel's fixed
worker commands in sidecar containers. A process here is any command, kept
running for as long as the container is.
Components¶
"components": {
"api": { "runtime": "go", "path": "api", "port": 8080 },
"web": { "runtime": "nodejs", "path": "web", "port": 3000 },
"worker": { "runtime": "python", "path": "worker" }
}
A monorepo as one project. Each component gets its own Dockerfile, compose
service and router, at <component>.<domain> unless domain says
otherwise. version, install, build and start work as in the runtime
block. No component can open a host port.
Sidecars¶
"sidecars": {
"chromium": { "image": "selenium/standalone-chromium:latest", "about": "Browser for Dusk", "env": { "SE_NODE_MAX_SESSIONS": "2" } }
}
A container beside the project that the catalogue does not provide. It is rendered into the project's own compose block, comes up and goes down with the project, and is not a shared service.
A full example¶
{
"name": "shop",
"domain": "shop.loc",
"aliases": ["admin.shop.loc"],
"runtime": "php",
"server": "nginx",
"document_root": "public",
"php": { "version": "8.4", "extensions": ["redis", "intl", "gd"] },
"services": ["mysql", "redis", "mailpit"],
"commands": {
"reindex": { "exec": ["php", "artisan", "app:reindex"], "about": "Rebuild the search index" }
},
"hooks": { "post-start": [["php", "artisan", "migrate", "--force"]] }
}
Two files beside it¶
| File | What it carries | Committed? |
|---|---|---|
.stackvo/site.json |
Environment variables for the container, directory listing, SSH agent forwarding. | Yes — it travels with the repository. |
stackvo.preset.json |
Service versions and the shareable settings that live in .env. Can never hold a secret. |
Yes — see Sharing and teams. |