stacks
Docker Compose stacks: one stacks row per stack, from which docker-compose.yml, .env and meta.json under the stacks directory (<state dir>/stacks/<name>/, /var/lib/hoserva/ stacks/<name>/ on an installed system) are generated. A stack's Compose project is run with docker compose, never through a shell.
List Compose stacks
Every stack Hoserva has a row for, sorted by name. A stack's `.env` is never returned: it holds generated secrets.
Create a Compose stack
Stores the stack's row (`.env` sealed under the machine key) and generates `docker-compose.yml`, `.env` and `meta.json` into the directory named after the stack, then checks the result with `docker compose config`. Nothing is started. Refused with 409 `stack_exists` when a stack of that name exists. A directory of that name already under the stacks directory (what a removed stack's own files left behind) is used as it is, and only the three generated files are written into it; it is refused with 409 `stack_dir_exists` when it holds a `docker-compose.yml`, which is never overwritten. A name that is not 1 to 63 lowercase letters, digits, `-` or `_`, starting with a letter or digit, is refused with 400 `invalid_stack_name` before anything is touched. A Compose file that `docker compose config` rejects is refused with 400 `invalid_stack`, and leaves no row and no generated file behind. A `.env` that defines one of the variables Docker needs from the daemon's environment (`PATH`, `HOME`, `XDG_RUNTIME_DIR`, `DOCKER_HOST`, `DOCKER_CONTEXT`, `DOCKER_CONFIG`, `DOCKER_CERT_PATH`, `DOCKER_TLS_VERIFY`) is refused with 400 `invalid_stack_env` before anything is stored or written, so Docker always runs with the daemon's own values for them.
Get a Compose stack
One stack's row with its stored `docker-compose.yml` text in `compose`, and `manuallyEdited`. Its `.env` is never returned: it holds generated secrets.
Edit a Compose stack's file
Replaces the stack's `docker-compose.yml` text. The text is first checked with `docker compose config` against a copy of the stack's own `.env`, in a temporary directory, before anything is stored or written: a file that is empty or that `docker compose config` rejects is refused with 400 `invalid_stack`, carrying the compiler's message, and leaves the row and every file as they were. With `dryRun` a valid file is answered with `applied` false and the stored stack, and nothing is changed. Otherwise the row is updated (the new text, and `manuallyEdited` set so a template form never silently overwrites it) and the stack's `docker-compose.yml` is regenerated from it; if writing the file fails the row is put back as it was. Nothing is restarted: `startStack` makes the edit take effect. A stack whose `.env` cannot be opened (a restore without the backup passphrase) cannot be checked and is refused. An unknown stack is refused with 404 `stack_not_found` and a name that is not a valid stack name with 400 `invalid_stack_name`.
Remove a Compose stack
Runs `docker compose down` for the stack (its containers and networks are removed; named volumes are kept), deletes its generated `docker-compose.yml`, `.env` and `meta.json`, and then its row; the stack's directory goes too if nothing else is in it, so the name can be used again and a file the stack keeps there stays. Taking the stack down does not need its `.env`. `docker compose down` removes every container and network of the stack's Compose project name, and with `--volumes` its named volumes, whichever file or directory they were started from. So before docker runs, every container of that project must be one Compose started from the stack's own directory: a project of the same name that something else runs (a hand-run `~/immich/compose.yml` is project `immich`) is refused with 409 `stack_project_shared`, whether or not the stack has its `.env` and whether or not appdata is deleted. Since that cannot be checked otherwise, a remove is also refused, as any docker failure is, when the project's containers cannot be listed (503 `docker_unavailable` when Docker is not reachable). Appdata is deleted only when `deleteAppdata` is explicitly true: then the stack's named volumes are removed with `docker compose down --volumes`, each bind-mount directory of the stack's containers that lies strictly inside an appdata location (the cache disk's `appdata` directory) and is used by no other container is deleted, and so is the stack's whole directory, before the row. Nothing is deleted if one of the stack's containers is still there after `docker compose down`. That needs the array running, like a container remove that deletes appdata: refused with 409 `array_stopped` (or 503 `array_state_unknown`) before anything is changed, with 409 `appdata_unavailable` when no appdata location is known, and with 409 `appdata_shared` when a directory is used by another container or another container binds a place inside the stack's directory. A name that is not a valid stack name is refused with 400 `invalid_stack_name` before anything is touched. A `docker compose down` that fails, or a directory that cannot be deleted, leaves the row in place so the remove can be retried.
Read an installed stack's template inputs
The inputs of the template a stack was installed from, with the value each has now. The inputs are read from the `x-hoserva` block of the stack's stored `docker-compose.yml` text, so the catalog is not needed and a stack whose Compose file was edited by hand is read as it runs (`manuallyEdited` says so), and the values from the stack's `.env`. A secret is never returned: `set` says whether the `.env` gives it a value. A `device` input is `readOnly`: its device mapping is part of the Compose file, which the form never writes. A `path` input that is not appdata lists the existing shares' paths as suggestions, as `previewTemplateInstall` does. A stack whose Compose file has no `x-hoserva` block (one made with `createStack` without a template) is refused with 409 `stack_has_no_template`; one whose block is not valid with 422 `template_invalid`; an unknown stack with 404 `stack_not_found` and a name that is not a valid stack name with 400 `invalid_stack_name`. A stack whose `.env` cannot be opened (a restore without the backup passphrase) is a plain failure.
Change an installed stack's template inputs
Changes the values of some of the stack's template inputs. Every value is checked with the rules `previewTemplateInstall` uses, before anything is stored: a value that does not fit its kind, a path that is not absolute, a `string` input emptied that is not optional, or a `device` input given a different value (its mapping is in the Compose file, so it is changed there) is refused with 400 `invalid_template_input`, whose `details.input` names the input; a `port` input changed to a port that a container publishes or is configured to publish, that another stack's Compose file publishes, or that the host listens on, is refused with 409 `no_free_port` and is never moved to another port (the stack's own ports are not taken); a `.env` that would define a variable Docker takes from the daemon's environment is refused with 400 `invalid_stack_env`. A refusal changes nothing. Otherwise the new `.env` is sealed into the stack's row and the stack's `.env` file is regenerated from it: only the lines of the inputs that changed are rewritten and every other line is kept. If writing the file fails the row is put back as it was. `docker-compose.yml` is never regenerated from the template, so a manual edit of it stays, and nothing is restarted: `startStack` makes the change take effect. An input without an entry keeps its value. An empty entry takes the input's default, as in an install, and so clears an optional `string` input. A secret with no entry or an empty one keeps the sealed value; a non-empty one replaces it; one named in `generate` gets a newly generated value (48 hexadecimal characters), and naming one that is not a secret, or that also has a value, is refused with 400 `invalid_template_input`. The answer is `getStackConfig`'s, with the values as they are now. The other errors of `getStackConfig` apply; 503 `docker_unavailable` is answered only when a port was changed, since the check needs Docker.
Start or recreate a Compose stack
Queues a `stack_start` job (service class, scoped to the stack) that runs `docker compose up --detach` for the stack: its services are created if they do not exist and recreated only where their definition changed, so an edit saved by `updateStack` takes effect. Missing generated files are written from the stack's row first. Refused with 409 `array_stopped` while the array is stopped (maintenance mode) or its storage is not ready, and with 503 `array_state_unknown` if the array's state cannot be read; no job is queued. The job checks again when it runs. An unknown stack is refused with 404 `stack_not_found` and an invalid name with 400 `invalid_stack_name`.