---
title: "ply.toml reference"
description: "Every key in the ply manifest — the [package] / [build] / [run] groups and all their fields."
canonical: https://plybox.sh/docs/manifest/
---

# ply.toml reference

The complete manifest surface, in three groups:

- **`[package]`** — identity (name, version, and the registry-facing fields).
- **`[build]`** — what makes the image: base, dependencies, include, sources.
- **`[run]`** — how it runs: entrypoint, env, ports, health, resources, … — all baked in as defaults, all overridable at deploy.

Only `[package]` is required.

```toml
[package]                         # identity
name = "myapp"                    # required; may not contain "-<digit>"
version = "1.2.0"                 # semver; part of the image filename
owner = "myname"                  # optional: registry namespace ply push uses
description = "One line for ply search / the registry"     # optional
license = "MIT"                   # optional: SPDX id or free text
homepage = "https://example.com"  # optional

[build]                           # what makes the image
base = "debian@13"                # exactly one base per app; or
                                  # { name = "debian", version = "13", source = "alias" }
include = ["dist/"]               # optional: ship only these paths (default: everything)

[build.dependencies]
node   = "22"                     # range: lowest satisfying version wins (MVS)
ffmpeg = { source = "alias", version = "6.1" }

[build.requires]
abi = "linux-x64-gnu"             # what the app layer's native deps were built against

[build.sources]                   # OPTIONAL — omit it and the official registry is used
alias = "github:org/repo"

[run]                             # how it runs — baked-in defaults, overridable at deploy
entrypoint = ["node", "server.js"]
user = "appuser:1000:1000"        # optional: run as name:uid:gid
workdir = "/opt/myapp"            # optional: cwd before exec (default: the app prefix)
stop_signal = "SIGTERM"           # optional: how to ask it to shut down
capabilities = []                 # optional: keep nothing (the default) — see below

[run.env]
NODE_ENV = "production"

[run.params]                      # optional: named values other apps read as {myapp.x} — see below
api_key = { secret = true }       # minted per stack; add external = true for BYO

[run.ports]
web = 3000                        # label of what the app binds — not a host claim

[run.volumes]
data   = "/var/lib/myapp"                                 # per-instance
shared = { path = "/srv/uploads", scope = "shared" }      # opt-in shared
cache  = { path = "/var/cache/myapp", ephemeral = true }  # GC-able

[run.network]                     # optional: the egress contract — see below
egress = ["api.stripe.com", "*.amazonaws.com", "1.1.1.1"]

[run.resources]
mem  = "512M"                     # memory.max (+ memory.high)
cpu  = "1.5"                      # cores
pids = 256                        # always enforced; default guards fork bombs

[run.health]
port  = 3000                      # TCP connect gate for deploys/restarts
grace = "30s"                     # cold-start budget

[run.restart]
policy = "on-failure"             # "never" (default) | "on-failure" | "always"
backoff = "1s"                    # doubles per failure…
max_backoff = "60s"               # …up to this cap; resets after healthy uptime
```

Small sections read fine as inline tables (`dependencies = { node = "22" }`,
`ports = { web = 3000 }`, `env = { NODE_ENV = "production" }`); the sub-table
form above is for when a section grows or wants comments. *(The older flat
form — every section at the top level, `base`/`entrypoint` under `[package]` —
is still accepted, so existing manifests keep building.)*

## Key notes

**`[package]`** — `name` + `version` produce the canonical filename
`<name>-<version>-<os>-<arch>.img`. Names may not contain `-` followed by a
digit (filename-parsing ambiguity). `entrypoint` is exec-style — an argv
array, no shell. To ask for one, write a **string** instead and it becomes
`["/bin/sh", "-c", <string>]`, which a TOML multi-line string makes readable:

```toml
entrypoint = """
[ -f /etc/caddy/Caddyfile ] || cp /opt/edge/Caddyfile /etc/caddy/Caddyfile
exec caddy run --config /etc/caddy/Caddyfile --watch
"""
```
 `user` makes ply create the passwd/group entry,
chown volumes, and drop privileges in the correct order. `base` names the
package that owns `/` (FHS, libc, `/bin/sh`) — `"name@range"`, or
`{ name, version, source }` to pin a source alias; it resolves, locks, and
fetches exactly like a dependency. In a base package's own manifest,
`base = true` marks it as one instead.

**`workdir`** — absolute path to `chdir` into before exec. Omit it and ply
uses the app's own prefix (`/opt/<name>`), which is right for anything ply
built. It exists mostly for [imported images](/docs/docker/), which carry a
`WORKDIR` their entrypoints depend on: start redis anywhere but `/data` and
its `find . -exec chown redis {} +` walks the entire filesystem.

**`stop_signal`** — the signal that means "shut down", default `SIGTERM`.
Not every daemon agrees: nginx drains on `SIGQUIT` and httpd on `SIGWINCH`,
and both would otherwise be killed mid-request when ply's patience runs
out. Case-insensitive, `SIG` optional — `quit`, `SIGQUIT` and `sigquit` are
the same request.

**`capabilities`** — what the app keeps after rights stripping. Omitting it
means **nothing**, which is the right answer for every package ply builds:
a native keg never chowns or setuids, because `user` does that from the
parent before stripping. Two other forms exist for the cases that need
them:

```toml
capabilities = "oci"                          # Docker's default fourteen
capabilities = ["chown", "net_bind_service"]  # exactly these
```

`"oci"` is what `ply import` writes, because official images assume
Docker's posture. Names are case-insensitive and the `CAP_` prefix is
optional; a typo fails at `ply build`, not at 3am. See
[Security & rootless](/docs/security/).

**`owner`, `description`, `license`, `homepage`** are optional and
registry-facing. `owner` is the namespace `ply push` publishes under —
set it here, or leave it out and pass `--as NAMESPACE` (or publish under
your own login). The other three surface on the registry page and in
`ply search`'s one-liner, read straight from this manifest; there is no
separate metadata file to keep in sync. See
[Registries & publishing](/docs/registries/).

**`[build.dependencies]`** — the key IS the package name. String values are
version ranges against the `default` source; table values pick a source
alias. Version syntax: `"22"` = any 22.x.y, `"6.1"` = any 6.1.x,
`"1.2.3"` = exactly 1.2.3. Resolution is
[Minimal Version Selection](/docs/dependencies/). Names containing a dot
must be TOML-quoted (`"boost1.84" = "1.84"`) — a bare dotted key means a
nested table in TOML.

**`[run.env]`** — composed after package contributions, before CLI overrides
(`-e`, `--env-file`); last wins. A value of the form `enc:v1:…` is a
[sealed secret](/docs/secrets/): committed as ciphertext, opened by the
run parent on the one host it was sealed for.

**`[run.params]`** — named values other apps interpolate with `{app.param}`,
and this manifest's own `[env]` can reference with bare `{param}`. Secrets,
computed values, and built-in facts (`host`, `port`, …) all go through the
same namespace. See below.

**`[run.ports]`** — documentation the tooling reads: `ply proxy` falls back to
these ports for an unpublished app, and `[health]` checks them. Never a host port
binding — that is `--publish`, deliberately a run-time decision rather than
a manifest one.

**`[run.volumes]`** — see [Volumes & data](/docs/volumes/). Per-instance by
default; `scope = "shared"` and `ephemeral = true` are the two modifiers.

**`[run.network]`** — `egress` is the claim: the outbound destinations this
package needs, as a list of hostnames, `*.suffix` wildcards, IPv4
addresses, CIDR ranges, or `*` for unrestricted. Omitting `[network]`
declares nothing (`ply inspect` shows `egress: not declared`); an empty
list (`egress = []`) is itself a claim — "this talks to nobody" — the
right shape for a database. The claim alone changes nothing at run time:
it takes a stack member's `egress = { mode, allow }` or `ply run
--egress`/`--egress-allow` to turn it into audit or enforcement. See
[Security & rootless](/docs/security/#egress-the-contract).

**`[run.resources]`** — cgroup v2 limits. `pids` is set even if you omit it.
`mem` and `cpu` take a fixed value (`"512M"`, `"1.5"`) or a range
(`{ min = "256M", max = "2G" }`) the run parent resizes live between —
see [Autoscaling](/docs/autoscale/).

**`[run.scale]`** — `min`, `max`, `signal` (`cpu`, `memory`, `net`,
`metric:<name>`), `target`, optional `cooldown` and `metrics_path`: the run
parent grows and shrinks the instance count on that signal. `min = 0` with
`idle = "10m"` lets the app sleep — the last instance stops after that long
with no connections and the next connection wakes it; `signal`/`target` are
then only needed when `max > 1`. Validated at `ply build`; details in
[Autoscaling](/docs/autoscale/).

**`[run.health]` / `[run.restart]`** — see
[Deploys, health & restarts](/docs/deploy/).

**`[build.requires]`** — declares the ABI your app layer's native artifacts were
built against; the resolver refuses mismatched runtimes loudly instead of
letting you segfault at 2am.

**`[run.requests]`** — host access the image asks for:
`links = ["/abs/host:/abs/container", …]`, or the spelled-out
`links = [{ host = "/abs/host", at = "/abs/container" }]`. Both paths must be
absolute in either spelling. Never applied on its own (a manifest ships inside the image — an image must
not grant itself host access); `ply run --grant-links` is the operator's
explicit yes, `ply systemd --grant-links` bakes the expansion into a unit.
Without the flag the requests are listed and not mounted.

**`[build.sources]`** — URL templates; see
[Registries & publishing](/docs/registries/). `{package}` expands to the
package name, letting one base URL serve per-package directories.
**The whole table is optional:** a dependency with no `source`, in a manifest
with no `[sources] default`, resolves from the official registry. Declare
`[sources]` when you actually fetch from somewhere else.

**`[run.volumes]`** — `name = "/path"` is the common form. The table form
(`{ path, scope, ephemeral }`) is for when you need `scope = "shared"` or
`ephemeral = true`.

## `[params]`

*(Grouped, this is `[run.params]` and the app configures itself from
`[run.env]`; the snippets below drop the `run.` prefix for brevity.)*

A param is a named value a package exposes; consumers interpolate it with
`{app.param}` — see [Stacks & local dev](/docs/stacks/) for the consumer
side. Here is the provider side, the shape a real keg ships:

```toml
[params]
user     = "postgres"                                               # plain value = default
database = "postgres"
password = { secret = true }                                        # minted per stack
url      = "postgres://{user}:{password}@{host}:{port}/{database}"  # computed — interpolates others

[env]                                  # the provider configures ITSELF from the same namespace
POSTGRES_USER     = "{user}"
POSTGRES_DB       = "{database}"
POSTGRES_PASSWORD = "{password}"
```

- A plain string is a default value. `{ secret = true }` mints a strong
  32-character value the first time a stack starts and stores it as a
  0600 file — never write a value alongside `secret = true`; minting *is*
  the default, so a manifest that tries fails to build. Add
  `external = true` and ply never mints: startup refuses until the
  operator provides the value (`ply secret set`, or a stack
  `params =`/`$VAR` override) — named, loud, no silent empty string.
- A computed param is just a param whose value contains `{other}` holes,
  resolved against the same package's own namespace. `url` above is not a
  special mechanism — it is a param that references its neighbors, same as
  any other.
- **Bare `{param}` in `[env]` reaches this same namespace** — declared
  params plus the built-in facts below — the moment a `[params]` table
  exists, even an empty one. A manifest with no `[params]` table is
  unchanged: `{`/`}` in `[env]` stay literal text, so nothing here breaks
  an existing manifest that hasn't opted in. Escapes: `{{` → literal `{`,
  `}}` → literal `}`. `$VAR` keeps its own rules and runs first — a value
  can mix both (`$VAR` reaches the ambient system, `{}` reaches the
  params namespace).
- **Built-in facts** — free on every package, no declaration needed:

  ```
  name  version  host  port  addr  base_url  scale  arch  image
  ```

  `addr` is `{host}:{port}`; `base_url` is `http://{host}:{port}` (http by
  convention). `port` is the container-side port of the first `--publish`,
  falling back to the package's own `[ports]` entry when it declares
  exactly one — so a keg that labels its port reads `{port}` even with no
  `--publish`. `host` is the `<name>.ply` address, which exists inside a
  stack and for a rootful run, but not for a bare rootless one.
  Referencing one that this run doesn't have is an error naming the gap,
  never a blank value — but only where it is *read*: a computed param
  nothing references (a keg's `url` on a run that publishes nothing) is
  simply unavailable, not fatal. `host`/`port` resolve per run mode
  (rootless loopback, rootful bridge, stack netns) — the reference is the
  same everywhere, the value is not.
- **Reserved names** — the built-ins above, plus the live set (populated by
  the runtime, never user-declared: `state instances started_at
  restarts`), plus `self` — cannot be redeclared in `[params]`:

  ```
  name version host port addr base_url scale arch image state instances started_at restarts self
  ```

  Referencing a *live* name (`state`, `instances`, `started_at`,
  `restarts`) from `[env]` or a computed param's default is a manifest
  error: those values change after launch, so baking one into env would be
  a stale-config bug waiting to happen. Read a live value from
  `/run/ply/self/<name>` at runtime, or wait on it with a stack `after`
  condition — see [Stacks & local dev](/docs/stacks/).

Images that carry `[params]` need ply 0.1.69 or newer — an older binary
rejects the unknown table outright (a manifest is parsed with unknown
fields denied), so update ply before pulling an image that declares one.

### Running a keg with params directly

`[params]`/`[env]` holes aren't only a stack thing: a bare `ply run` on an
image whose manifest declares `[params]` resolves that manifest's own
hole-y `[env]` too, against the app's own declared defaults and facts —
`ply run postgres@17 -e POSTGRES_PASSWORD=dev` gets `POSTGRES_USER` and
`POSTGRES_DB` defaulted to `postgres`/`postgres` with no further flags,
and either is still overridable with `-e`. A declared param nothing in
`[env]` reads never gets in the way: postgres's computed
`url = "…@{host}:{port}/…"` wants an address a rootless run doesn't have,
and that only matters if something asks for `url`.

A hole that reads a **secret** param is different: a standalone run has
nowhere durable to keep a minted value the way a stack does, so it refuses
to start rather than silently mint one or leave the literal `{password}`
in the child's environment:

```
postgres: [env] POSTGRES_PASSWORD reads a secret param — pass -e POSTGRES_PASSWORD=… , or run it from a stack (ply up mints secrets)
```

Pass the value yourself (`-e POSTGRES_PASSWORD=dev`) for a standalone run,
or run the same image inside a stack instead — `ply up` mints and stores
it. An explicit `-e KEY=value` always wins and is never re-resolved,
whether or not the key it names reads a secret. A manifest with no
`[params]` table is untouched by any of this.

## Two more files, same grammar

**`[[service]]`** — a `ply.toml` with `[[service]]` blocks is a
**composition**: several apps wired for `ply up`, under the same `[package]`
header an app uses (name, owner, version, description). `owner` is the
registry namespace `ply push` publishes it under — same `[package] owner` as
an app. It is the `[[service]]` array that makes it a composition — a
`[package]` header alone is still just an app. Each member is
`run = "postgres@17"` (registry app), `run = "./server"` (local app dir), or
`run = "git+https://…"` (a repo the host builds), plus `name`, `env`,
`params`, `after`, `publish`, `domain`, `volume`, `scale`. Registry members
pin into the dir's `ply.lock` (`ref`, `version`, `digest.<arch>`). See
[Compositions](/docs/stacks/). *(Legacy files use `[[app]]` blocks and a
`[stack]` header; both are still read.)*

**`ply.dev.toml`** — a gitignorable dev overlay next to an app's ply.toml,
applied only by `ply run DIR` / `ply up` (never by `build` — a shipped
image cannot contain dev configuration). Keys: `entrypoint` (replaces),
`[env]` (merges; `-e` still wins), `links` (extra binds; relative host
paths resolve against the app dir, relative container paths land under
`/opt/<name>/`). See [Stacks & local dev](/docs/stacks/).
