The stack manifest
stack.toml sits at the root of a stack directory, beside compose.yml, and
each service is described in a file of its own under services/, which the
root’s include list names. Everything lemonfiber knows about the stack comes
from these files: the services, the role each plays, the forms you start them
by, and how they are wired to each other. It knows nothing about Sonarr that the
manifest does not declare, so adding a service is a new file and an include
entry, and not a new release.
Every field is below, read out of
contract/stack-manifest.schema.json and contract/stack-service.schema.json
at the revision this site pins, which are generated from the types lemonfiber
reads a stack into.
The rule each field is held to, with its reason, is
the stack manifest contract.
The stack lemonfiber ships, and one of your own
Section titled “The stack lemonfiber ships, and one of your own”The stack lemonfiber ships is built into the binary. The build refuses to produce a binary whose built-in manifest it cannot read, so that stack is checked before it ever reaches you.
To run a stack of your own, point lemonfiber at its directory:
$ lemonfiber --stack-dir <PATH> ps--stack-dir is a global flag, so it goes with any
command. A directory of your own is checked when lemonfiber loads it. To add a service to it, see
adding a service.
The three versions it carries
Section titled “The three versions it carries”schema_version = 1stack_version = "0.1.0"min_cli_version = "0.1.0"schema_versionis the generation of the file’s format.stack_versionis the version of what the file describes. It moves when services or forms change.min_cli_versionis the oldest lemonfiber that may operate the stack.
A schema_version this lemonfiber does not read and a min_cli_version newer
than it are refused with different codes, so the message says which one is
wrong.
When lemonfiber refuses a stack
Section titled “When lemonfiber refuses a stack”lemonfiber checks the whole manifest before it starts anything. It reports
every fault it finds in one pass, and each one names where it is. The
refusal is a STACK code:
| What you see | What to do |
|---|---|
STACK-1 |
No readable stack.toml is where a stack was expected. Point --stack-dir at the directory that holds it. |
STACK-7 |
The file is not valid TOML. Fix it at the line the message names. |
STACK-10 |
The files are not laid out as a stack is: a service still in stack.toml, a file under services/ that include does not name, or an entry naming no file. Fix each file the message names. |
STACK-2 |
The schema_version is one this lemonfiber does not read. Update lemonfiber, or use a stack written for this version. |
STACK-9 |
The min_cli_version is newer than the lemonfiber running. Run lemonfiber update self. |
STACK-8 |
The file names things this lemonfiber has no meaning for, usually because it comes from a newer lemonfiber. Update lemonfiber, or change what the message names. |
STACK-6 |
The manifest contradicts itself. Common causes are a floating image tag, an id declared twice, or a form naming a profile nobody declares. |
The full list of rules is the contract’s validation section.
Every field
Section titled “Every field”The reference starts at the two roots: Manifest is stack.toml, and
StackServiceFile is one services/<id>.toml. Every type a field names links to
its own entry.
Manifest
A whole stack manifest.
| Field | Type | Required | What it is |
|---|---|---|---|
form | array ofForm | no | Every declared form. |
include | array ofstring | no | The file each service is declared in, |
min_cli_version | string | yes | The oldest |
profile | array ofProfile | no | Every declared profile. |
removed | array ofRemoved | no | Every service this stack used to carry and no longer does. Optional, and deliberately: a stack that has never dropped anything has nothing to record, and requiring an empty table of it would make the record something to satisfy rather than something to read. |
schema_version | integeruint32, ≥ 0 | yes | The manifest format generation. |
stack_version | string | yes | The content version — which services and forms this stack has. |
wiring | array ofWiring | no | Every link between two of this stack's services. Optional in the format because an operator's own stack directory stays readable without it, and because a stack written before this table existed is still a stack. It is not optional of the one this project ships: a link that is not declared here is one nothing can report on, substitute at, or show as the exception it is. |
StackServiceFile
One service's file, services/<id>.toml: exactly one [[service]], whose id is the name the file is filed under, and nothing else.
| Field | Type | Required | What it is |
|---|---|---|---|
service | array ofService | yes |
Bind
Which interface a service's port is published on.
One of these:
"loopback"Reachable only from the host.
"lan"Reachable from the local network.
Claim
The core capability a plugin claims, and the probes it is demonstrated by.
The vocabulary owns what must be shown; this owns where to ask. A capability is one contract with many claimants, each answering at a path of its own, so the published probe declares the question and the statuses that answer it, and the binding declares the method, the path and the recorded response.
| Field | Type | Required | What it is |
|---|---|---|---|
capability | string | yes | A core name, which must also appear in the service's |
probe | array ofClaimProbe | no | One binding per probe the capability declares. Every one of them, exactly once. |
ClaimProbe
One probe of a claimed capability, bound to a request on this plugin's service or, where the vocabulary declares the probe a run, to a run of its image's own program.
| Field | Type | Required | What it is |
|---|---|---|---|
expect | Expect | yes | What the answer must be, within what the probe permits. |
fixture | string | yes | The recorded response. Required here where it is optional on a proof: a claim nobody can demonstrate without owning the service is a claim the catalogue's own checks cannot make. |
id | string | yes | Names a probe the capability declares. |
request | PluginRequestornull | no | Method and path on this plugin's own service. Exactly one of this and |
run | PluginRunornull | no | Arguments to the image's own entrypoint, run with no network. Exactly one of this and |
Criticality
How much a service's absence costs.
Serialisable as well as readable, because it reaches an operator: a status report that says a service is down without saying whether that matters leaves them to guess, and the manifest already holds the answer.
One of these:
"critical"Its failure has consequences outside the machine.
"core"The form does not work without it.
"important"The form works, badly.
"enhancing"Makes things better.
"optional"Nice to have.
Dashboard
A service's entry on the household's dashboard, where its wiring names any of it.
| Field | Type | Required | What it is |
|---|---|---|---|
description | stringornull | no | The line beside it. |
group | stringornull | no | The group it appears under. |
hidden | boolean | no | Whether it has no entry. |
icon | stringornull | no | The icon it shows, from the dashboard's own icon set. |
Expect
What the answer has to be.
A status is a claim about the network path rather than about the service: Docker publishes a port by putting a proxy in front of it, and that proxy accepts a connection before knowing whether anything inside is listening. So a status alone is not evidence — except for a refusal or a redirect to HTTPS, answers no port proxy produces.
A key of the four key-wise constraints is a place rather than a name. A plain name is a top-level member, and one beginning with / is a JSON Pointer, extended with a step that picks an entry of a list by a field it holds. A flat name was enough while every service answered a flat object, and the only thing it could say about a service that nests its payload was that the envelope was there — which is a probe that passes by observing that something replied.
| Field | Type | Required | What it is |
|---|---|---|---|
body_starts_with | stringornull | no | What the body must begin with, where it is not JSON. |
content_type | stringornull | no | A substring of the content type the answer was served as. |
exit | integerornullint64 | no | The exit status a run must end with. Not sufficient alone. |
json | map ofExpectedornull | no | Places the answer must carry, each with the exact value it must hold. |
json_array_min | integerornulluint64, ≥ 0 | no | The answer read as an array, with at least this many entries. A catalogue is very often a list rather than an object, and none of the object-shaped constraints can say anything about one. |
json_at_least | map ofintegerint64ornull | no | Places the answer must carry, each with a number it must not be below. |
json_has_keys | array ofstringornull | no | Places the answer must carry, whatever they hold. |
json_is_absent | booleanornull | no | The answer did not parse as JSON at all. Which is what a service that serves its application shell for every path it does not implement answers — and the reason a status alone proves nothing against one: the shell comes back |
json_types | map ofExpectedKindornull | no | Places the answer must carry, each with the kind of value it must be. |
status | integerornulluint16, ≥ 0, ≤ 65535 | no | The status the answer must carry. |
stdout_starts_with | stringornull | no | What a run's standard output must begin with. |
Expected
Exactly what a place must hold.
Three kinds and no nesting: a flag that must be set, a number that must match, or a word. A value deeper than this is asking about a document rather than about a claim — and where the thing worth asserting is deeper in the answer, the key reaches it rather than the value growing to match.
It is booleanorintegerint64orstring
ExpectedKind
The kind of value a key must hold.
Five, and closed. A name outside them is one no runner could evaluate, and an assertion nothing evaluates is a proof that silently checks less than it says — which is worse than one that fails.
Named for what it is the kind of, rather than Kind, because this is published and whoever generates from it flattens every definition into one scope. Kind is the one name there a generator is certain to want for itself: every envelope this contract describes is keyed by its kind, so the union of them is a Kind too, and two of them in one module is a definition nothing can be compiled against.
One of these:
"bool"A true or a false.
"int"A whole number.
"str"A string.
"list"An array.
"dict"An object.
Form
A named slice of the stack an operator can run.
| Field | Type | Required | What it is |
|---|---|---|---|
composable | boolean | no | Whether this form may be combined with others. |
description | string | yes | One line, plain language. |
id | string | yes | Unique. |
name | string | yes | Human-facing name. |
profiles | array ofstring | yes | The complete closure, written out rather than inferred. |
Health
How to tell a service has started.
| Field | Type | Required | What it is |
|---|---|---|---|
kind | HealthKind | yes | Which kind of probe to use. |
path | stringornull | no | HTTP path, for |
timeout_s | integerornulluint32, ≥ 0 | no | How long to wait before calling it unhealthy. |
HealthKind
The kinds of health probe a service can declare.
One of these:
"http"Fetch a path on the service's port.
"tcp"Open a connection to the service's port.
"container"Trust the container's own reported state.
PluginRequest
What is asked, and where.
Three fields and no more, and the third is the one worth explaining. A service that answers XML unless a caller asks for JSON cannot satisfy a capability whose probe requires a JSON assertion, and until this field there was nowhere to ask: Plex answers text/xml at every path, including the one its health probe uses, unless the request carries Accept: application/json.
It is one media type and not a header map, and the difference is the point. A probe declares who it is asked as, and none on every guarded probe has meant what it says partly because nothing could be presented. A map of headers would make that a convention a reviewer has to hold — any service may name its credential header whatever it likes, so no list of refused names could ever be closed — where one named field keeps it a property of the format. A probe still cannot present anything, because there is nowhere to write it.
| Field | Type | Required | What it is |
|---|---|---|---|
accept | stringornull | no | The one representation the answer is asked for, as a media type. Absent where the service needs no asking, which is most of them. |
method | string | yes | The HTTP method. |
path | string | yes | The path on the service being asked. |
PluginRun
A run of the image's own program, as a proof declares it.
| Field | Type | Required | What it is |
|---|---|---|---|
args | array ofstring | yes | Each argument handed to the image's entrypoint, one string each: one to 64, each at most 4096 bytes and none holding a NUL. |
timeout_s | integerornulluint32, ≥ 0 | no | How long the run may take, from 1 to 120 seconds. Default 30. |
PluginShape
A privileged shape lemonfiber writes for a service.
One of these:
"egress-guard"NET_ADMINand the tunnel device."render"The host's render devices and the group that owns them.
Profile
A Compose profile — one service's role in the stack.
| Field | Type | Required | What it is |
|---|---|---|---|
description | string | yes | Shown in form previews. |
id | string | yes | Unique, and matches a Compose profile name exactly. |
name | string | yes | Human-facing name. |
protocol | StackProtocolornull | no | The provider this profile cannot run without, where it needs one. Absent means it is never narrowed away. Declaring it here rather than letting the tool recognise profile names by sight is what keeps a renamed profile from silently losing its guard. |
RecordedWith
What a recording gives a service in the operator's place: a constant reaching no third party, or, written "minted", a value made for that run alone.
It is stringorintegerint64orboolean
Removed
A service this stack used to carry, and what became of it.
Kept beside the services rather than in a changelog because of who asks and when: an operator who remembers a service and cannot find it is holding a gap, and a gap is closed by a record that travels with the stack they are running rather than by a file they would have to know exists. It versions with the stack for the same reason every other per-service fact here does — the stack decided the removal, so the stack is what carries the answer, and a lemonfiber release is not needed to say what became of a service lemonfiber never chose.
replaced_by is optional because the honest answer is sometimes that nothing replaced it. Recording a replacement that does not exist to avoid an empty field is worse than the gap it fills.
| Field | Type | Required | What it is |
|---|---|---|---|
id | string | yes | The id it was declared under, which is the name an operator will look for. |
reason | string | yes | Why it went — a sentence, not a word. |
removed_in | string | yes | The A stack version rather than a date, because the manifest already carries one and an operator asking what became of a service is holding a stack rather than a calendar: which release of this stack stopped having it is the question a date could only be translated back into. |
replaced_by | stringornull | no | The service that took its place, where one did. |
Secret
A value a service holds, declared before anything captures or enters it.
| Field | Type | Required | What it is |
|---|---|---|---|
carried_from | SecretCarriedFromornull | no | Where the service a part moves in place of wrote this credential itself. |
for | stringornull | no | The capability of the plugin's own that a credential crossing to it is needed to fill. |
id | string | yes | What the value is, within its namespace. |
minted | SecretMinted | no | Whether lemonfiber makes the value at install, rather than anything capturing or entering it, and in what shape. |
of | string | yes | Whose credential it is. |
recorded_with | RecordedWithornull | no | What a recording gives the service in the operator's place, where only the operator holds the value and the service needs one to start. |
why | string | yes | What holding it is for. |
SecretCarriedFrom
Where a service wrote a credential itself: a file inside its container, the format it is written in and the dotted path to the value.
| Field | Type | Required | What it is |
|---|---|---|---|
file | string | yes | The file, as a path inside the service's container beneath its configuration directory. |
format | SecretFormat | yes | The format the file is written in. |
key | string | yes | The dotted path to the value in the file; for |
SecretFormat
A format a service writes its configuration in.
One of these:
"xml"XML.
"json"JSON.
"yaml"YAML.
"ini"INI.
SecretMinted
How a manifest writes [Minted]: true or false, or the shape it is made in.
It is booleanorSecretMintedShape
SecretMintedShape
A shape a minted value is made in, beyond a key.
One of these:
"password"A password: every character class a password rule asks for.
Service
One container in the stack.
| Field | Type | Required | What it is |
|---|---|---|---|
asks_for | stringornull | no | What it asks for out there, in the same terms. Declared with [ |
bind | Bindornull | no | Which interface the port is published on. |
claim | array ofStackClaim | no | The evidence for what [ Kept as the tables the stack wrote rather than read here. A claim is the plugin contract's shape and that contract's reader is what holds it, so a bundled service and a plugin standing in for it meet one set of binding rules; a second reader here would be a second answer to what a claim may say, and this crate cannot reach the first, which reads this one. |
config_path | stringornull | no | Where inside the container its one configuration directory is mounted, |
config_ro | boolean | no | Whether its configuration directory is mounted read-only. |
criticality | Criticality | yes | How much its absence costs. |
dashboard | Dashboard | no | Its entry on the household's dashboard, as the plugin the bundle assembles it from wires it. A stack directory's service declares none. |
depends_on | array ofstring | no | Same-profile dependencies only. |
describes | string | yes | What it does for the operator. |
digest | stringornull | no | The digest of the image's multi-architecture index, What actually runs. A digest names one index for good, where the tag beside it could be moved to a different one tomorrow, so the stack resolves every image by this and a service without one is refused by validation. Optional here so that refusal can name the service, rather than the file failing to parse. |
env | map ofServiceValue | no | Its environment, each variable one of four typed kinds of value. |
grants | array ofstring | no | Extra kernel capabilities granted to the container, checked against an allow-list. Named The old spelling is still accepted, because an operator's own stack description is theirs and a rename is not a reason to refuse to read it. |
health | Healthornull | no | How to tell the service has actually started. |
host_managed | boolean | no | True where the OS owns the lifecycle rather than Compose. |
id | string | yes | Unique, and matches the Compose service name. |
image | string | yes | Image reference without a tag. |
last_release | string | yes | The latest release upstream has published, |
license | string | yes | SPDX identifier. |
listens | integerornulluint16, ≥ 0, ≤ 65535 | no | The port it answers on inside the stack's network, where lemonfiber and the services that ask for what it provides reach it. Not always the port it publishes: |
media_types | array ofstring | no | Which media types it handles. |
mem_limit_mib | integerornulluint32, ≥ 0 | no | The most memory the container may hold, in MiB. |
memory_mib | integerornulluint32, ≥ 0 | no | The memory it expects to need, in MiB. The stack's estimate, and only ever shown as one: a form's footprint is the sum of these over the services it would start, and a figure read as a measurement is one an operator believes and acts on. Optional because a stack that says nothing has not said zero, and the services that are silent are named beside the sum rather than counted as costing nothing. |
mount | array ofServiceMount | no | Each further path mounted from its own mount tree. |
name | string | yes | Human-facing name. |
network | ServiceNetworkornull | no | The capability whose filler's network it runs inside. |
port | integerornulluint16, ≥ 0, ≤ 65535 | no | Primary UI or API port, absent for services with no listener. |
posture | ServicePosture | no | How tightly the container is held. |
profile | string | yes | Exactly one profile. |
provides | array ofstring | no | What this service can be asked for, as named capabilities. An open list of strings here and checked for shape only, deliberately. The published vocabulary is the set these names come from, and it is generated from this field — a reader that held the set as well would be a cycle, and one that held a copy of it would be a second answer to what the vocabulary carries. The generator refuses a name the vocabulary does not carry, by name. |
publish | array ofServicePublish | no | Each further port it publishes on its tier. |
reaches | stringornull | no | Where this service's own requests go, in the terms an operator would recognise — not in protocol names. An empty string is an answer: this service reaches nothing. That is not the same as the field being absent, which is the stack declining to say, and the two must never be read as one — an inventory of what leaves a machine that rendered "we were not told" as "nothing leaves" would be making a privacy claim out of its own ignorance. Optional because a stack written before this existed says nothing, and a build that refused such a manifest would refuse the stack it ships with. Where it is absent, lemonfiber answers from what it knows about the services it ships, and reports that it knows nothing where it does not. |
secret | array ofSecret | no | The values it holds, each declared by the plugin the bundle assembles it from and held in its own id's namespace. A stack directory's service declares none. |
shape | PluginShapeornull | no | The one privileged shape lemonfiber writes for it, from a closed set. |
tag | string | yes | Explicit version — a floating tag is a validation failure. The version an operator reads, and nothing more: a tag is a label its publisher can move to another image, so what runs is named by [ |
takes_data | boolean | no | Whether it takes the data mount. |
tmpfs | array ofstring | no | Absolute paths given scratch space in memory. |
upstream | string | yes | Project URL, for maintenance review. |
user_mode | ServiceUserModeornull | no | Who the container runs as. |
without_it | string | yes | The consequence of not running it. |
ServiceConstant
A value written exactly as declared.
It is stringorintegerint64orboolean
ServiceMount
A further path mounted from the service's own mount tree.
| Field | Type | Required | What it is |
|---|---|---|---|
from | string | yes | The relative path inside the mount tree. |
ro | boolean | no | Whether it is mounted read-only. |
to | string | yes | The absolute path inside the container. |
ServiceNetwork
The network a service runs inside instead of its own.
| Field | Type | Required | What it is |
|---|---|---|---|
via | string | yes | The capability whose filler's network it runs inside. |
ServicePosture
How tightly the container is held.
One of these:
"standard"Six kernel capabilities, no privilege gained, a bounded process count.
"confined"A read-only root and no kernel capability at all, besides.
ServicePublish
A further port a service publishes on its tier.
| Field | Type | Required | What it is |
|---|---|---|---|
listens | integerornulluint16, ≥ 0, ≤ 65535 | no | Where it answers inside the container, |
port | integeruint16, ≥ 0, ≤ 65535 | yes | Where it is published on the host. |
ServiceSecretOf
A secret the manifest declares.
| Field | Type | Required | What it is |
|---|---|---|---|
secret | string | yes | The declared secret's name. |
ServiceSetting
An operator setting, with what is written while none is recorded.
| Field | Type | Required | What it is |
|---|---|---|---|
default | ServiceConstant | yes | What is written while the operator has recorded no value. |
recorded_with | RecordedWithornull | no | What a recording gives the service in the operator's place, where only the operator holds the value and the service needs one to start. |
setting | string | yes | The setting's name. |
ServiceUrlOf
The address of a capability's filler.
| Field | Type | Required | What it is |
|---|---|---|---|
files | stringornull | no | The medium the filler asked for files. |
native | stringornull | no | The upstream's own API, where the ask reaches the upstream an adapter fronts. |
url_of | string | yes | The capability asked for. |
ServiceUserMode
Who the container runs as.
One of these:
"image"No user: the image decides.
"puid-env"PUIDandPGIDset to the operator's."uid"The operator's uid and gid as the container's user.
ServiceValue
One environment variable's value.
It is ServiceConstantorServiceSettingorServiceUrlOforServiceSecretOf
StackClaim
The artefact says nothing more about it.
It is Claim
StackProtocol
A download provider a profile can depend on.
Serialisable as well as readable, for the same reason [Criticality] is: it reaches an operator. A profile left out of a closure is only half reported without the provider it wanted.
One of these:
"usenet"Needs a Usenet provider.
"torrent"Needs a VPN and a torrent client.
Wiring
One link between two of the stack's services, and which of the two it names.
A link asks for a capability or it names a service, and the difference is the whole subject. An ask is written against what the far end does, so anything that stands in for it is reached by everything that asked and nothing else changes; a name is written against what the far end is, which is sometimes the honest answer and is always the exception.
Both halves of the pair are optional in the type and exactly one of them is required by validation. Making it an enum in the parser would report a link that carried both as a shape failure, and this has two ends where it should have one is a sentence a parse error cannot say.
| Field | Type | Required | What it is |
|---|---|---|---|
asks | stringornull | no | The capability asked for, where this is an ask. |
by | string | yes | The service the link runs from — what asked. The name a report uses when nothing fills what was asked for. Without it an unfilled capability is a fact about the stack with nobody to tell, which is the failure at the point of use this exists instead of. |
each | boolean | no | Whether the link reaches every service that fills it rather than the one. Some asks are for the one service that does a thing and some are for all of them, and how many claimants happen to exist today does not say which this is. A stack where one capability is declared four times over and another once would have both reported as the same answer, and only one of the two would be a link that worked. |
filled_by | stringornull | no | Which claimant fills it, where the stack's own services contest it. A default the operator substitutes, not a rule: install order, precedence and recency are each a way of being right most of the time, and a choice written down with its reason beside it is what is done instead of them. |
to | stringornull | no | The service named, where this link is by name. |
why | stringornull | no | Why it is by name, or why that claimant was chosen. Required of both, because an exception with no reason beside it reads as an oversight and a choice with no reason beside it reads as a rule. |
Where to go next
Section titled “Where to go next”Adding a service is the same file from the editing side. Running without lemonfiber is the Compose project underneath it. Forms and slices is what the forms are for.