Skip to content

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:

Terminal window
$ 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.

schema_version = 1
stack_version = "0.1.0"
min_cli_version = "0.1.0"
  • schema_version is the generation of the file’s format.
  • stack_version is the version of what the file describes. It moves when services or forms change.
  • min_cli_version is 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.

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.

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.

FieldTypeRequiredWhat it is
formarray ofFormno

Every declared form.

includearray ofstringno

The file each service is declared in, services/<id>.toml, in the order the services are listed. This list alone decides which services the stack holds: a file in services/ it does not name is refused, not read.

min_cli_versionstringyes

The oldest lemonfiber this stack will work with.

profilearray ofProfileno

Every declared profile.

removedarray ofRemovedno

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_versionintegeruint32, ≥ 0yes

The manifest format generation.

stack_versionstringyes

The content version — which services and forms this stack has.

wiringarray ofWiringno

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.

FieldTypeRequiredWhat it is
servicearray ofServiceyes

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.

FieldTypeRequiredWhat it is
capabilitystringyes

A core name, which must also appear in the service's provides.

probearray ofClaimProbeno

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.

FieldTypeRequiredWhat it is
expectExpectyes

What the answer must be, within what the probe permits.

fixturestringyes

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.

idstringyes

Names a probe the capability declares.

requestPluginRequestornullno

Method and path on this plugin's own service. Exactly one of this and run, and this where the vocabulary declares the probe a request.

runPluginRunornullno

Arguments to the image's own entrypoint, run with no network. Exactly one of this and request, and this where the vocabulary declares the probe a run.

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.

FieldTypeRequiredWhat it is
descriptionstringornullno

The line beside it.

groupstringornullno

The group it appears under.

hiddenbooleanno

Whether it has no entry.

iconstringornullno

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.

FieldTypeRequiredWhat it is
body_starts_withstringornullno

What the body must begin with, where it is not JSON.

content_typestringornullno

A substring of the content type the answer was served as.

exitintegerornullint64no

The exit status a run must end with. Not sufficient alone.

jsonmap ofExpectedornullno

Places the answer must carry, each with the exact value it must hold.

json_array_minintegerornulluint64, ≥ 0no

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_leastmap ofintegerint64ornullno

Places the answer must carry, each with a number it must not be below.

json_has_keysarray ofstringornullno

Places the answer must carry, whatever they hold.

json_is_absentbooleanornullno

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 200 whether the API behind it exists or not, so what has to be said is that the body was not a document.

json_typesmap ofExpectedKindornullno

Places the answer must carry, each with the kind of value it must be.

statusintegerornulluint16, ≥ 0, ≤ 65535no

The status the answer must carry.

stdout_starts_withstringornullno

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.

FieldTypeRequiredWhat it is
composablebooleanno

Whether this form may be combined with others.

descriptionstringyes

One line, plain language.

idstringyes

Unique.

namestringyes

Human-facing name.

profilesarray ofstringyes

The complete closure, written out rather than inferred.

Health

How to tell a service has started.

FieldTypeRequiredWhat it is
kindHealthKindyes

Which kind of probe to use.

pathstringornullno

HTTP path, for http probes.

timeout_sintegerornulluint32, ≥ 0no

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.

FieldTypeRequiredWhat it is
acceptstringornullno

The one representation the answer is asked for, as a media type.

Absent where the service needs no asking, which is most of them.

methodstringyes

The HTTP method.

pathstringyes

The path on the service being asked.

PluginRun

A run of the image's own program, as a proof declares it.

FieldTypeRequiredWhat it is
argsarray ofstringyes

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_sintegerornulluint32, ≥ 0no

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_ADMIN and 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.

FieldTypeRequiredWhat it is
descriptionstringyes

Shown in form previews.

idstringyes

Unique, and matches a Compose profile name exactly.

namestringyes

Human-facing name.

protocolStackProtocolornullno

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.

FieldTypeRequiredWhat it is
idstringyes

The id it was declared under, which is the name an operator will look for.

reasonstringyes

Why it went — a sentence, not a word.

removed_instringyes

The stack_version whose catalogue no longer carries it.

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_bystringornullno

The service that took its place, where one did.

Secret

A value a service holds, declared before anything captures or enters it.

FieldTypeRequiredWhat it is
carried_fromSecretCarriedFromornullno

Where the service a part moves in place of wrote this credential itself.

forstringornullno

The capability of the plugin's own that a credential crossing to it is needed to fill.

idstringyes

What the value is, within its namespace.

mintedSecretMintedno

Whether lemonfiber makes the value at install, rather than anything capturing or entering it, and in what shape.

ofstringyes

Whose credential it is.

recorded_withRecordedWithornullno

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.

whystringyes

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.

FieldTypeRequiredWhat it is
filestringyes

The file, as a path inside the service's container beneath its configuration directory.

formatSecretFormatyes

The format the file is written in.

keystringyes

The dotted path to the value in the file; for ini, its section and entry.

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.

FieldTypeRequiredWhat it is
asks_forstringornullno

What it asks for out there, in the same terms.

Declared with [Self::reaches] or not at all: one without the other is half an answer, and validation refuses it by name rather than quietly using the half that is there.

bindBindornullno

Which interface the port is published on.

claimarray ofStackClaimno

The evidence for what [Self::provides] names: one claim per capability, each binding the capability's probes to requests on this service and to recordings.

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_pathstringornullno

Where inside the container its one configuration directory is mounted, /config where absent.

config_robooleanno

Whether its configuration directory is mounted read-only.

criticalityCriticalityyes

How much its absence costs.

dashboardDashboardno

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_onarray ofstringno

Same-profile dependencies only.

describesstringyes

What it does for the operator.

digeststringornullno

The digest of the image's multi-architecture index, sha256: and 64 lower-case hex digits.

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.

envmap ofServiceValueno

Its environment, each variable one of four typed kinds of value.

grantsarray ofstringno

Extra kernel capabilities granted to the container, checked against an allow-list.

Named grants rather than capabilities because the word is about to mean something else entirely: what a service can do, which is how a plugin declares itself and how wiring asks for a filler. Two meanings under one spelling is how a setting that decides what a container may do to the kernel gets read as a description of what it offers.

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.

healthHealthornullno

How to tell the service has actually started.

host_managedbooleanno

True where the OS owns the lifecycle rather than Compose.

idstringyes

Unique, and matches the Compose service name.

imagestringyes

Image reference without a tag.

last_releasestringyes

The latest release upstream has published, YYYY-MM-DD.

licensestringyes

SPDX identifier.

listensintegerornulluint16, ≥ 0, ≤ 65535no

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: SABnzbd publishes 8085 and answers on 8080. Declared rather than known, so a service standing in for another is reached at the port it answers on rather than at the one the service it replaced did.

media_typesarray ofstringno

Which media types it handles.

mem_limit_mibintegerornulluint32, ≥ 0no

The most memory the container may hold, in MiB.

memory_mibintegerornulluint32, ≥ 0no

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.

mountarray ofServiceMountno

Each further path mounted from its own mount tree.

namestringyes

Human-facing name.

networkServiceNetworkornullno

The capability whose filler's network it runs inside.

portintegerornulluint16, ≥ 0, ≤ 65535no

Primary UI or API port, absent for services with no listener.

postureServicePostureno

How tightly the container is held.

profilestringyes

Exactly one profile.

providesarray ofstringno

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.

publisharray ofServicePublishno

Each further port it publishes on its tier.

reachesstringornullno

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.

secretarray ofSecretno

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.

shapePluginShapeornullno

The one privileged shape lemonfiber writes for it, from a closed set.

tagstringyes

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 [Self::digest].

takes_databooleanno

Whether it takes the data mount.

tmpfsarray ofstringno

Absolute paths given scratch space in memory.

upstreamstringyes

Project URL, for maintenance review.

user_modeServiceUserModeornullno

Who the container runs as.

without_itstringyes

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.

FieldTypeRequiredWhat it is
fromstringyes

The relative path inside the mount tree.

robooleanno

Whether it is mounted read-only.

tostringyes

The absolute path inside the container.

ServiceNetwork

The network a service runs inside instead of its own.

FieldTypeRequiredWhat it is
viastringyes

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.

FieldTypeRequiredWhat it is
listensintegerornulluint16, ≥ 0, ≤ 65535no

Where it answers inside the container, port where absent.

portintegeruint16, ≥ 0, ≤ 65535yes

Where it is published on the host.

ServiceSecretOf

A secret the manifest declares.

FieldTypeRequiredWhat it is
secretstringyes

The declared secret's name.

ServiceSetting

An operator setting, with what is written while none is recorded.

FieldTypeRequiredWhat it is
defaultServiceConstantyes

What is written while the operator has recorded no value.

recorded_withRecordedWithornullno

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.

settingstringyes

The setting's name.

ServiceUrlOf

The address of a capability's filler.

FieldTypeRequiredWhat it is
filesstringornullno

The medium the filler asked for files.

nativestringornullno

The upstream's own API, where the ask reaches the upstream an adapter fronts.

url_ofstringyes

The capability asked for.

ServiceUserMode

Who the container runs as.

One of these:

  • "image"

    No user: the image decides.

  • "puid-env"

    PUID and PGID set 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.

FieldTypeRequiredWhat it is
asksstringornullno

The capability asked for, where this is an ask.

bystringyes

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.

eachbooleanno

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_bystringornullno

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.

tostringornullno

The service named, where this link is by name.

whystringornullno

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.

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.