What compose is for
A worker is a single process with an identity on the engine. A real deployment is several of them with an order between them: a database before the API, the API before the web front end, a migration before either. The engine holds workers and routes calls to them. Something has to own the order, the environment each process starts with, and the response when one of them dies. That is compose. Aworker-compose.yaml declares the group, and a daemon turns the declaration into
running processes it keeps watching.
The compose environment
Compose files provide reproducibility. A project starts the same way from a login shell, a systemd unit, or a CI runner, because the daemon’s own environment plays no part in it. Everything a container needs is in the compose file, which makes the file a complete description of how the project runs and makes a reviewer able to see the whole contract in one place. Strictness in the compose file serves the same end and ensures that an incomplete “system” cannot be started accidentally.Three names, three jobs
A compose call names three things, and confusing any two of them is the mistake worth naming up front. The daemon namespace is which machine. It comes from--namespace, and it is where that daemon
answers compose::*, so iii trigger compose::up --namespace dev reaches one daemon and not its
neighbour. Several daemons attach to one engine, which is what lets compose supervise workers where
their resources are rather than only beside the engine.
The file is which project. A daemon holds as many as it is given, and the compose file is the
only thing that identifies one.
The project namespace is where that project’s workers register. It comes from namespace: in the
compose file: the engine’s routing dimension, the same one every other worker uses.
Why the project has no name of its own
An earlier design gave each project an id the operator chose on the firstup. It read well and was
wrong, because it was a second identity for something the file already identified. Two identities
have to be kept in agreement, and the failure was silent in both directions: an id could be pointed
at a different file, and a mistyped id became a new empty project reporting that it had nothing to
stop, reporting success for a command that did nothing.
Deriving the project from its file removes the question. The same file reached twice is the same
project however it was spelled, a mistyped file is a file that will not open, and an error existed
only to police the divergence that can no longer happen.
The namespace stays exactly what the compose file says, so an operator can read it off the file and
type it into iii trigger --namespace or a worker.trigger call. Predictability is what makes a
namespace usable by hand, and it comes from being declared rather than derived.
Two copies of one project therefore share a project namespace and collide, which is how the engine
reports a duplicate for every other worker as well.
For the routing dimension itself and how the engine handles a contested name, see
Namespaces.
Why the daemon owns seven variables
A container’s environment is its own, with seven exceptions the daemon sets and refuses to let a container replace. The rule is not that static configuration outranks an environment variable, which would be the wrong way round for most settings. It is that each of these seven is already declared somewhere in the compose file, and a second declaration of the same thing is a disagreement nobody resolves.III_URL is the daemon’s connection. Readiness is observed over it, so a container pointed at
another engine is invisible to the daemon that started it, however healthy it is. The failure would
arrive as a startup timeout over a worker that is running and serving, which is the least
diagnosable shape a failure can take. Two engines mean two daemons.
III_NAMESPACE and III_WORKER_NAME are the pair readiness watches. Letting a container change
either would mean compose waiting in one place while the child registers in another, so the override
would have to be threaded through readiness, the child record and compose::status before it could
work at all. Both are already declared: the namespace by the file, the name by the container key.
III_COMPOSE_NAMESPACE, III_COMPOSE_FILE, and III_COMPOSE_DIR identify the supervisor and
project that started the container. The project namespace cannot route to the daemon namespace, and
one daemon can supervise several files, so a managed worker needs the namespace and file to make an
explicit, unambiguous compose::* call. The directory is the canonical parent of the file and gives
workers one stable base for project-owned data. Letting the container replace these values could
send a lifecycle edit to another project or write data outside that project.
III_CONFIG_NAME identifies the configuration service entry. Compose reads the current value,
merges execution overrides, and calls configuration::set with flush: false before spawn.
Workers read that value through the same GET. Explicit saves persist the submitted object.
Removing an override does not restore an older disk value. No snapshot file is delivered. The retired III_CONFIG name is rejected in explicit
environment and env_file declarations with RETIRED_CONFIG_ENV; stale inherited values are
removed. Matching is case-sensitive on Unix and follows native case-insensitive rules on Windows.
A container that belongs in another namespace
The case the reserved contract genuinely refuses is a container joining a namespace other than its project’s, a shared one addressed by two projects for instance. That is not an oversight. A namespace is declared per file, and a project is its file, so a container that registers somewhere else is describing a different project. Declaring it in a second compose file says exactly that, and keeps the property that reading one file tells you where everything in it lands.Why state belongs to the project
A project’s process records, resolved configuration, worker output and VM state are stored in<project-dir>/.iii/compose/<namespace>/. Its managed engine uses the same directory for its lock,
generated configuration and log. The project directory comes from the canonical compose file path,
so running iii compose --up --file from another directory keeps state beside that file.
The engine lock belongs to the project and namespace together. Two checkouts can each use default
with engines on different ports. A second managed invocation for the same project and namespace is
refused. Within one engine, each Compose daemon still needs its own namespace.
For read-only checkouts, III_COMPOSE_STATE_DIR moves project state to
$III_COMPOSE_STATE_DIR/<project-slug>/<namespace>/. The slug includes a hash of the canonical
compose path so different checkouts remain separate under a shared root. compose::status reports
the resolved state_dir for either layout. The default layout requires a .iii/compose/ entry in
the project’s ignore rules to exclude generated state from version control.
Installed packages remain shared at ~/.iii/compose/packages, or $III_COMPOSE_STATE_DIR/packages.
The cache is keyed by name, version and target, so projects can reuse downloaded workers.
Engine observed readiness
Compose determines ready state through the engine rather than locally as this is the one way to ensure dependencies are ready for a given worker. For example whenapi starts after database,
start_after guarantees the engine can already route a trigger to database, so api can reliably
use the database dependency from boot. A check on the process alone would guarantee only that
something was launched.
The engine’s view is also detailed enough to report clear statuses to the user.
Scoped shutdown
When one container in anup fails, compose stops what that operation started, in reverse order,
and leaves everything else running. The rule is that an operation undoes itself, which makes up
safe to retry.
Teardown follows the graph backwards, so dependents stop before the containers they depend on and
nothing is left using a worker that no longer exists. A container that stops on its own takes the
same path, so its dependents come down in the same order as a deliberate stop.
The blast radius depends on the clock
Those two sentences describe two different rules, and it is worth being plain about the gap between them. During anup, the first container that fails ends the operation: everything that operation
started is rolled back, and everything after it in the start order is never attempted. On a first
up of a five-container project, a failure in the last one leaves the whole project down, including
containers that have nothing to do with it. Once a container is ready, the supervisor is narrower:
it takes that container’s transitive dependents down and leaves the rest alone.
So a mailer that nothing depends on would end the whole start if it failed during up, and be
contained if it failed a minute later. The same declaration, the same container, two blast radii
separated only by timing.
Each rule is useful for a different project. Compose therefore makes the start-time choice part of
the file instead of assuming that every container has the same blast radius.
Saying it in the file
A container is not required by default. Its failed start is reported against that container, nothing is rolled back, and the operation carries on. Setrequired: true when one container must
make the operation fail:
required_default to set the fallback for every container in a file. A container-level value
always wins:
state inherits true, while queue remains false. When neither required nor
required_default is present, the effective value is false.
Dependents of a non-required container carry on too. A container that names it in start_after
starts as if it had come up, because start_after is a start order rather than a claim that the
dependent cannot run without it. A dependent that genuinely cannot run without it says so by
failing on its own.
That moves what status: ok means. It used to say every planned container is up; it now says every
required one is, so the return names the rest in not_required_failures rather than leaving a
caller to compare the plan against a later status call. A successful result has no top-level error.
required controls the result after Compose finishes trying. A second field controls whether
Compose retries before it accepts that result.
Retry policy
A container declares what Compose does when its first start fails or when it exits after it was ready:no is the default. A failed first start settles immediately, and an exit after Ready takes the
container’s transitive dependents down. on-failure retries a failed start or a non-zero run-time
exit. A clean run-time exit with on-failure is recorded as stopped. always retries a clean
run-time exit, which is the answer for a worker that is only correct while it is running.
A supervised restart is the same act as compose::restart: one container stops and starts, and the
graph around it is left alone. So its dependents stay up while it is gone. This is the same reasoning
that non-required starts use: start_after is a start order rather than a claim that the dependent
cannot run without it. What that costs is a dependent holding a connection that drops and has to
reconnect, which is the cost the file asked for by declaring a policy at all.
Replacement attempts are capped at five. The first retry is immediate. Later retries wait from
500ms up to a ceiling of 30 seconds. Both limits are load-bearing: a policy with no backoff turns a
crash loop into a busy loop, and a policy with no cap never lets the operator find out. A container
that holds ready for a minute has recovered, so its run-time budget refills. A worker that crashes
once an hour is therefore restarted every time, rather than five times ever.
When the budget runs out the supervisor does what it would have done with no policy at all. It fails
the container, takes its dependents down, and says which in the log. That is the shape worth
keeping: restart changes how many times compose tries, and never what happens when trying is over.
compose::status reports a container waiting on a run-time replacement as restarting. It is
not ready, because nothing is running under that name, and not failed, because the supervisor
has not given up on it. It has no PID until the next process starts. During up, the active progress
row shows the retry attempt, its wait, and the successful recovery.
Related
For the function surface, the compose file schema, and the error codes, see Using iii /
Compose. For how workers reach each other once compose has started them,
see Using iii / Functions.