# desired.json (/docs/reference/desired-state)



The document a host watches at the path `paths.desired_state_file` names, and converges on with
every change to it: which apps run here, on which volumes, reachable how.
[Deploy an app](/docs/getting-started/deploy-an-app) is the guide to it; this is the file field
by field. A document the host cannot read is refused whole, and `reported.json` says so.

An editor completes and checks a document that names the schema:

```json
{
  "$schema": "https://raw.githubusercontent.com/ilbertt/nibrunner/main/crates/protocol/schema/desired-state.schema.json",
  "hostId": "host-1"
}
```

Schema: [desired-state.schema.json](https://raw.githubusercontent.com/ilbertt/nibrunner/main/crates/protocol/schema/desired-state.schema.json)

## HostDesiredState [#hostdesiredstate]

What one host should be running. The daemon watches this document at the path its `paths.desired_state_file` names and converges on every change to it.

| Name          | Type                                             | Description |
| ------------- | ------------------------------------------------ | ----------- |
| `hostId`      | [HostId](#hostid)                                |             |
| `volumes`     | array of [DesiredVolume](#desiredvolume)         |             |
| `instances`   | array of [DesiredInstance](#desiredinstance)     |             |
| `checkpoints` | array of [DesiredCheckpoint](#desiredcheckpoint) |             |
| `exports`     | array of [DesiredExport](#desiredexport)         |             |

## HostId [#hostid]

an identifier

string, matching `^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$`

## DesiredVolume [#desiredvolume]

| Name                         | Type                                        | Description                            |
| ---------------------------- | ------------------------------------------- | -------------------------------------- |
| `volumeId`                   | [VolumeId](#volumeid)                       |                                        |
| `appId`                      | [AppId](#appid)                             |                                        |
| `sizeBytes`                  | integer, at least 0                         |                                        |
| `desiredState`               | [DesiredPresence](#desiredpresence)         |                                        |
| `initialContents` *optional* | [InitialContents](#initialcontents) \| null | Absent for a volume that starts empty. |

## DesiredInstance [#desiredinstance]

| Name                       | Type                                                 | Description                                                                                                                                                                        |
| -------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `appId`                    | [AppId](#appid)                                      |                                                                                                                                                                                    |
| `deploymentId`             | [DeploymentId](#deploymentid)                        | A running instance is replaced when this changes, and only then: a new layer or config under the same `deploymentId` is not picked up.                                             |
| `volumeId`                 | [VolumeId](#volumeid)                                | One of this document's `volumes`, mounted in the guest as the app's data directory.                                                                                                |
| `desiredState`             | [DesiredInstanceState](#desiredinstancestate)        |                                                                                                                                                                                    |
| `layers`                   | array of [DesiredLayer](#desiredlayer), 1 to 8 items | The root filesystem, bottom layer first. At least one; at most `MAX_LAYERS`.                                                                                                       |
| `config`                   | [AppConfig](#appconfig)                              |                                                                                                                                                                                    |
| `hostnames`                | array of [AppHostname](#apphostname)                 | What the HTTP proxy routes to this app's `httpPort`. Empty for an app nothing outside needs to reach by name.                                                                      |
| `activation` *optional*    | [ActivationPolicy](#activationpolicy) \| null        | What puts this instance to sleep and what tells the host it is ready. A `sleepWhen` other than `never` is refused on anything but an `on-request` instance.                        |
| `idleTimeoutMs` *optional* | [IdleTimeoutMs](#idletimeoutms) \| null              | How long an `on-request` instance stays up after its last request before it sleeps: the older spelling of `activation.sleepWhen`, refused beside it. 300000 when neither is named. |

## DesiredCheckpoint [#desiredcheckpoint]

| Name           | Type                                | Description |
| -------------- | ----------------------------------- | ----------- |
| `checkpointId` | [CheckpointId](#checkpointid)       |             |
| `volumeId`     | [VolumeId](#volumeid)               |             |
| `desiredState` | [DesiredPresence](#desiredpresence) |             |

## DesiredExport [#desiredexport]

| Name                     | Type                                            | Description |
| ------------------------ | ----------------------------------------------- | ----------- |
| `exportId`               | [ExportId](#exportid)                           |             |
| `appId`                  | [AppId](#appid)                                 |             |
| `volumeId`               | [VolumeId](#volumeid)                           |             |
| `objectKey`              | [ObjectKey](#objectkey)                         |             |
| `desiredState`           | [DesiredPresence](#desiredpresence)             |             |
| `environment` *optional* | [TenantEnvironment](#tenantenvironment) \| null |             |

## VolumeId [#volumeid]

an identifier

string, matching `^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$`

## AppId [#appid]

an identifier

string, matching `^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$`

## DesiredPresence [#desiredpresence]

One of `"present"`, `"absent"`.

## InitialContents [#initialcontents]

What a volume holds before its app has written a byte: an archive in the store — a tar, gzipped or not, or a zip — unpacked under `destinationPath` in the app's root as the volume is formatted. That happens once, so this is read once: a volume already formatted is its app's, and a change here does nothing to it.

| Name              | Type                          | Description                                                                                                                                                                   |
| ----------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `digest`          | [Sha256Digest](#sha256digest) |                                                                                                                                                                               |
| `objectKey`       | [ObjectKey](#objectkey)       | Where the object lives in the store.                                                                                                                                          |
| `destinationPath` | [GuestPath](#guestpath)       | The directory the archive's entries land under, as the app sees it. Made if no layer holds it, and given — with everything unpacked into it — to the uid the program runs as. |

## DeploymentId [#deploymentid]

an identifier

string, matching `^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$`

## DesiredInstanceState [#desiredinstancestate]

The whole of an instance's activation policy.

| Value          | Description                                                                                                                                                |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"running"`    | Keeps the microVM up.                                                                                                                                      |
| `"on-request"` | Brings the microVM up for the first deploy and for every request that finds it asleep, and lets it sleep again once it has been quiet for `idleTimeoutMs`. |
| `"stopped"`    | Takes the microVM down and leaves the app reachable enough to say so.                                                                                      |

## DesiredLayer [#desiredlayer]

One read-only layer of the root filesystem an instance boots into. Layers stack in the order the document lists them, first at the bottom, and the app's volume is stacked writable over all of them. The kind says what the object is, and so what the host does with it.

One of the following, told apart by `kind`:

### filesystem [#filesystem]

A squashfs or ext4 image, attached as it was uploaded.

| Name        | Type                          | Description                          |
| ----------- | ----------------------------- | ------------------------------------ |
| `kind`      | `"filesystem"`                |                                      |
| `digest`    | [Sha256Digest](#sha256digest) |                                      |
| `objectKey` | [ObjectKey](#objectkey)       | Where the object lives in the store. |

### executable [#executable]

One program, packed into an image at `destinationPath` and run the way this host has always run one: by the guest's own init, with the app's arguments and environment.

| Name              | Type                              | Description                          |
| ----------------- | --------------------------------- | ------------------------------------ |
| `kind`            | `"executable"`                    |                                      |
| `digest`          | [Sha256Digest](#sha256digest)     |                                      |
| `objectKey`       | [ObjectKey](#objectkey)           | Where the object lives in the store. |
| `destinationPath` | [ExecutablePath](#executablepath) |                                      |

## AppConfig [#appconfig]

| Name               | Type                                    | Description                                                                                       |
| ------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `httpPort`         | [HttpPort](#httpport)                   | The guest port the HTTP proxy sends this app's hostnames to, and the one the health check probes. |
| `command`          | [Command](#command)                     |                                                                                                   |
| `resources`        | [InstanceResources](#instanceresources) |                                                                                                   |
| `healthCheck`      | [HealthCheck](#healthcheck)             |                                                                                                   |
| `restartPolicy`    | [RestartPolicy](#restartpolicy)         |                                                                                                   |
| `ports` *optional* | array of [InstancePort](#instanceport)  | What this app answers on besides `httpPort`, if anything.                                         |

## AppHostname [#apphostname]

| Name       | Type                                | Description |
| ---------- | ----------------------------------- | ----------- |
| `hostname` | [Hostname](#hostname)               |             |
| `kind`     | [AppHostnameKind](#apphostnamekind) |             |

## ActivationPolicy [#activationpolicy]

| Name        | Type                        | Description |
| ----------- | --------------------------- | ----------- |
| `sleepWhen` | [SleepPolicy](#sleeppolicy) |             |

## IdleTimeoutMs [#idletimeoutms]

integer, 60000 to 86400000

## CheckpointId [#checkpointid]

an identifier

string, matching `^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$`

## ExportId [#exportid]

an identifier

string, matching `^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$`

## ObjectKey [#objectkey]

between 1 and 1024 characters

string, 1 to 1024 characters

## TenantEnvironment [#tenantenvironment]

map of [TenantValue](#tenantvalue), keys matching `^[A-Za-z_][A-Za-z0-9_]*$`

## Sha256Digest [#sha256digest]

a lowercase hex sha-256

string, matching `^[0-9a-f]{64}$`

## GuestPath [#guestpath]

a guest path

string, at most 4096 characters, matching `^/$|^(/(?!\.\.?(/|$))[^/\\"'\x00-\x1f]+)+$`

## ExecutablePath [#executablepath]

an absolute path to a file other than /sbin/init

string, at most 4096 characters, matching `^/$|^(/(?!\.\.?(/|$))[^/\\"'\x00-\x1f]+)+$`

## HttpPort [#httpport]

integer, 1 to 65535

## Command [#command]

What the guest runs once its root is stacked, as uid 65534: `program` with `args`, in `workingDirectory`, with `environment`. The working directory is made if it is not there, given to that uid, and is where the program's persistent state lives, because everything under the root persists on the volume and nowhere else is the program's to write.

| Name               | Type                                    | Description |
| ------------------ | --------------------------------------- | ----------- |
| `program`          | [GuestPath](#guestpath)                 |             |
| `args`             | [TenantArguments](#tenantarguments)     |             |
| `workingDirectory` | [GuestPath](#guestpath)                 |             |
| `environment`      | [TenantEnvironment](#tenantenvironment) |             |

## InstanceResources [#instanceresources]

| Name        | Type                | Description |
| ----------- | ------------------- | ----------- |
| `vcpuCount` | integer, at least 0 |             |
| `memoryMib` | integer, at least 0 |             |

## HealthCheck [#healthcheck]

What tells this host an instance is well — and so what a wake waits for before it hands the caller on, and what the instance's liveness is read from after that. Named in every document: a default would be a kind nobody chose, and the one that looks obvious lies. A TCP connect is answered by the guest kernel's accept queue whether or not the process behind it will ever read the request, so a program that has stopped answering passes it for as long as it lives.

One of the following, told apart by `kind`:

### http [#http]

`path`, requested on `httpPort`, answers 2xx.

| Name                 | Type                | Description |
| -------------------- | ------------------- | ----------- |
| `kind`               | `"http"`            |             |
| `path`               | string              |             |
| `intervalMs`         | integer, at least 0 |             |
| `timeoutMs`          | integer, at least 0 |             |
| `gracePeriodMs`      | integer, at least 0 |             |
| `healthyThreshold`   | integer, at least 0 |             |
| `unhealthyThreshold` | integer, at least 0 |             |

### tcp [#tcp]

A connection to `httpPort` is accepted. Only that — for a port that does not speak HTTP.

| Name                 | Type                | Description |
| -------------------- | ------------------- | ----------- |
| `kind`               | `"tcp"`             |             |
| `intervalMs`         | integer, at least 0 |             |
| `timeoutMs`          | integer, at least 0 |             |
| `gracePeriodMs`      | integer, at least 0 |             |
| `healthyThreshold`   | integer, at least 0 |             |
| `unhealthyThreshold` | integer, at least 0 |             |

### boot-completed [#boot-completed]

The microVM is up and `httpPort` has accepted a connection once: the tenant is listening. That is all it is asked, and only until it answers — a guest this host did not build answers no path it was not told about — so its liveness after that is the microVM being up.

| Name   | Type               | Description |
| ------ | ------------------ | ----------- |
| `kind` | `"boot-completed"` |             |

## RestartPolicy [#restartpolicy]

| Name               | Type                | Description |
| ------------------ | ------------------- | ----------- |
| `maxRestarts`      | integer, at least 0 |             |
| `initialBackoffMs` | integer, at least 0 |             |
| `maxBackoffMs`     | integer, at least 0 |             |
| `backoffFactor`    | number              |             |
| `resetAfterMs`     | integer, at least 0 |             |

## InstancePort [#instanceport]

A port a guest answers on beside its HTTP one, carried to it unread.

It carries whatever arrives, tcp or udp: a port is a port, the relay in front of a host forwards both for every one in its range, and a guest that listens on only one of them answers the other with a port-unreachable the way any host would.

| Name        | Type                    | Description |
| ----------- | ----------------------- | ----------- |
| `name`      | [PortName](#portname)   |             |
| `guestPort` | [GuestPort](#guestport) |             |

## Hostname [#hostname]

a hostname

string, at most 253 characters, matching `^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$`

## AppHostnameKind [#apphostnamekind]

One of `"platform"`, `"custom"`.

## SleepPolicy [#sleeppolicy]

What puts a running microVM back to sleep. Only an `on-request` instance may carry one that fires: nothing else on this host would wake it again, and the next reconcile pass would bring it straight back up.

One of the following, told apart by `kind`:

### never [#never]

| Name   | Type      | Description |
| ------ | --------- | ----------- |
| `kind` | `"never"` |             |

### traffic-idle [#traffic-idle]

| Name        | Type                            | Description |
| ----------- | ------------------------------- | ----------- |
| `kind`      | `"traffic-idle"`                |             |
| `timeoutMs` | [IdleTimeoutMs](#idletimeoutms) |             |

### max-lifetime [#max-lifetime]

| Name    | Type                            | Description |
| ------- | ------------------------------- | ----------- |
| `kind`  | `"max-lifetime"`                |             |
| `ttlMs` | [MaxLifetimeMs](#maxlifetimems) |             |

## TenantValue [#tenantvalue]

Handed to the app as is, except that `${NAME}` and `$NAME` are filled in for NAME in NIBRUN\_HOSTNAME, NIBRUN\_HTTP\_PORT; any other `$NIBRUN_` reference is refused.

string, at most 32768 characters

## TenantArguments [#tenantarguments]

The arguments the binary is started with.

array of string, at most 4096 characters, at most 64 items

## PortName [#portname]

lowercase letters, digits and dashes, starting with a letter

string, matching `^[a-z][a-z0-9-]{0,15}$`

## GuestPort [#guestport]

integer, 1 to 65535

## MaxLifetimeMs [#maxlifetimems]

integer, 60000 to 604800000
