worker-compose.yaml, starts
each container in dependency order, waits for the engine to register it, and supervises it
afterwards.
The daemon is itself a worker. It registers under the name compose in a namespace of its own and
exposes the compose::* functions there, so every project operation is a normal
trigger, addressed to one daemon with --namespace.
The daemon
A daemon holds any number of projects, and any number of daemons share one engine. What tells them
apart is the namespace: the worker name is always
compose, so the engine leases (namespace, compose) to one connection. Two daemons with different namespaces coexist; a second claiming one
that is taken is refused at registration with DAEMON_ALREADY_SERVING.
Without --namespace the daemon generates one and prints it: two words, so it can be read once and
typed from memory. There is no well-known default, because a shared name is the collision the
namespace exists to prevent. A generated name never reuses one that already holds state on this
machine, and the engine refuses a second daemon claiming a namespace that is already served.
SIGINT and SIGTERM both stop the daemon and take every project down with it. compose::stop
does the same over the engine.
Starting a project with the daemon
iii compose up serves and brings one project up, without waiting for a call to name it.
--file defaults to ./worker-compose.yaml, the same fallback a compose::* call gets when it
names no file. The daemon then stays in the foreground and serves, so every other operation is a
compose::* call as usual.
PROJECT_DID_NOT_START. Rollback has already
stopped whatever came up, so there is nothing left to supervise.
Running it in the background
Compose never backgrounds itself. It serves in the foreground and writes to stdout, which is the shape every process supervisor already expects, and it leaves log rotation and restart-on-failure to something that already does both. For a quick session, a shell redirect is enough:The compose::* functions
All eight functions accept the same payload.
A project is its compose file, and nothing else names one. The same file reached twice is the same
project however it was spelled, so there is no second identity to keep in sync and no way to point
one at the wrong file.
file is not required. Left out, it falls back to a worker-compose.yaml in the daemon’s own
working directory. worker has no fallback.
Adding a worker
compose::add declares a worker in the compose file and restarts the project.
worker= takes a registry name (state), a name with a version (state@0.21.4)
or a directory (./workers/api); a leading . or / is what makes it a path,
since a registry reference may carry a host of its own.
An unpinned name is resolved once and written out as an exact version, so a
later up cannot quietly get a different build. The same worker at the same
version changes nothing and says so; at a different version it is replaced,
which is how an upgrade or a rollback is asked for.
A worker is rarely alone. Its manifest names what it calls, and the registry
answers with that whole graph already pinned to versions that satisfy each
other, so those are declared too, as containers with their own depends_on.
Nothing starts behind the file: what runs is still what the file says, and an
operator can read it, pin it differently, or take a container out.
Two rules shape the expansion. Workers compiled into the engine are skipped,
because they are already serving and have no artefact to install; an edge to
one is dropped rather than written, since depends_on may only name a
container the file declares. And a worker two others need is declared once,
named by both.
The file is edited, not rewritten: comments, blank lines and quoting survive,
entries are appended, and the result is parsed before it is written, so a bad
edit never reaches disk. What is written is worker, version and
depends_on. It writes no scripts, so a path:// worker needs
scripts.start in its own iii.worker.yaml to start. A path:// worker is added alone: its
dependencies are declared in a manifest on disk rather than in the registry’s
answer.
file out when the daemon’s own working directory holds a
worker-compose.yaml; without one, it fails with NO_COMPOSE_FILE. A relative file resolves
against the daemon’s directory, not the caller’s, so pass an absolute path when they differ.
compose::restart is a down followed by an up, with the compose file read again between them,
so an edit made by hand takes effect without restarting the daemon. It accepts container like the
two halves it is made of. Nothing cleverer yet: no rolling restart, and no keeping a container that
did not change.
compose::stop stops a compose project but returns before the daemon exits.
compose::validate answers for a file and holds nothing: a CI job that only ever validates leaves
the daemon owning nothing. Validation is offline, so package:// containers are reported under
deferred_packages instead of being resolved.
Restarting one worker
compose::restart worker=state stops that container and starts it again. Nothing else moves: not
what it depends on, and not what depends on it.
That is a decision, not a shortcut. A dependent holding a connection to the worker sees it drop, and
compose does not restart the dependents to hide it. Which of them tolerate a drop is the operator’s
knowledge, not compose’s. Left out, worker restarts the whole project instead: down, then up, with the file
read again between them.
Updating a worker
compose::update worker=state moves a declared container to another version of the same package.
changed is false.
The container has to be declared already, and has to be a package:// one: this edits a version
line, it does not add a container, and a path:// worker has no version to move. Use
compose::add to declare something new.
Only the version line changes. A depends_on written by hand comes through as it was, since
rewriting the graph is what compose::add is for.
Unlike
compose::restart worker=, an update restarts the whole project. A project is held as its
file was read, so a new version is only picked up once the project is dropped and read again.
Dropping it while its other containers run would leave them supervised by nothing.Operation results
compose::up and compose::down return the same fields.
Each entry in
containers contains container, state, changed. If a container failed it will
also contain an error object with code and message.
A failed up tears down any running containers, in reverse startup order, and reports those
containers as stopped with changed: false. Containers that were already running before the
operation are left alone.
Container states
compose::status reports each declared container with its state, its pid, an owned flag, and
last_error when there is one. owned is false for a container this daemon can see but did not
start.
Validation reports
worker-compose.yaml
This documentation reflects version 1 of a worker compose file. Unknown keys and duplicate keys
are errors. Durations can specify a unit:
500ms, 30s, 2m.Top-level fields
The namespace must already be
[a-z0-9_-]. A value outside that set is refused with
INVALID_NAMESPACE rather than rewritten to fit, so what the file declares is what an operator
types into --namespace. Nothing about the file’s path enters it, so two copies of one project
collide instead of running side by side.
Container fields
Each key undercontainers is the worker name the container registers under.
path:// directories resolve against the compose file’s directory. A missing directory fails with
MISSING_WORKER_DIRECTORY, and a missing env_file fails with MISSING_ENV_FILE during
validation.
Worker kinds
The registry answers with a kind, and it decides how the container runs. A kind compose cannot run fails withUNSUPPORTED_PACKAGE_KIND.
A bundle is published code that compose did not build, so it is started behind the same boundary
iii add puts it behind, rather than as a host process. Its configuration is published into the
guest, so III_CONFIG names a path inside the VM; a worker reads it the same way either way.
Bundle support can be refused machine-wide with III_BUNDLE_WORKERS_DISABLED=1, which compose
honours.
A bundle’s VM is booted by iii-worker, which the installer ships beside iii and which needs
glibc on Linux. Compose runs it as a process rather than linking it, so the engine stays portable;
a bundle container on a machine without iii-worker fails saying so, and every other worker kind
is unaffected.
Bundles need a VM, and windows has none: a bundle container there fails with
BUNDLE_NEEDS_A_VM before anything is downloaded. Run compose under WSL, where the VM has KVM to
run on. Every other worker kind runs on windows as it always has.
Scripts
Both hooks run with the container’s environment, working directory, and their own process group.
The start command for a
path:// container is run, then scripts.start from the worker’s
iii.worker.yaml. A container with neither fails with MISSING_START_COMMAND.
Where the two files describe the same thing, worker-compose.yaml wins and the manifest is the
default. run overrides scripts.start, and the container key overrides the manifest’s name: the
key is what reaches the child as III_WORKER_NAME, so a worker honouring the reserved contract
registers under it whatever its manifest declares. A worker that hardcodes its name instead is
caught at readiness by WORKER_NAME_MISMATCH, which reports the name it took.
Configuration precedence
Lowest to highest: the configuration a package ships, the entry the configuration worker holds, thenconfig_override. The merged result is written to an owner-only file and its path is passed to the
container as III_CONFIG. A container that declares config_name does not start when the fetch
fails; the error is CONFIG_FETCH_FAILED.
The container environment
A container’s environment is built, and the daemon’s own environment is not inherited wholesale. Three layers apply, lowest to highest.- A host baseline. On Unix:
PATH,HOME,USER,LOGNAME,SHELL,TERM,TMPDIR,TZ,LANG,LC_ALL. Windows adds the variables the platform needs, such asSystemRoot,COMSPEC, andPATHEXT. - The container’s
env_fileentries, then itsenvironmentmap. - The reserved variables, which the daemon owns.
Declaring a reserved variable in
environment or an env_file fails with RESERVED_ENV_OVERRIDE,
in both cases at compose::validate time.
For why the daemon owns these five rather than treating them as defaults a container can replace,
see Understanding iii / Compose.
Reading a value from the host
The baseline is short on purpose, so nothing an operator exported reaches a worker by accident. A file names the values it wants instead, with${VAR}.
environment: a worker path, a version, an env_file
entry. The file on disk is never rewritten.
What config_override keeps
config_override is never expanded, at any depth. That block is not compose’s to read: it is
carried to the configuration worker, which resolves ${VAR} references of its own at read time.
That is how a secret is stored as a reference rather than as a value.
Readiness
A container is considered up when the engine reports a worker of that name in the project’s namespace. Compose pollsengine::workers::list every 200 ms until the container’s
startup_timeout runs out.
After a container is ready, the daemon checks it every 250 ms. A container that exits takes its
transitive dependents down with it and is recorded as
failed. Nothing is restarted: v1 has no
restart policy, so a container that dies stays down until the next up. When the engine connection
drops and comes back, every running container gets its startup_timeout to register again.
What a failure takes with it depends on when it happens, and v1 has no way to declare otherwise.
So a container nothing depends on ends the whole
up if it fails at start, and is contained if it
fails a minute later. See Understanding iii / Compose.Where compose keeps state
Everything sits under~/.iii/compose, or under $III_COMPOSE_STATE_DIR when that is set.
<ns> is the daemon’s namespace. <project> is derived from the compose file’s canonical path:
readable enough to recognise, hashed enough that two projects in directories of the same name stay
apart. Because it is derived, it cannot be guessed. compose::status reports it as state_dir,
which is how a container’s startup output is located:
For why this is one place per machine rather than a directory beside each compose file, see
Understanding iii / Compose.
failed, and a live pid it
cannot verify is left running and reported for manual cleanup.
Error codes
Compose errors cross the wire with a stable code and a message.Related
For why compose is a worker and why a project is its file, see Understanding iii /
Compose. For the namespace dimension itself, see Understanding iii
/ Namespaces. For the
iii compose entry in the command tree,
see the CLI reference.