Instances
An instance is a running copy of an environment on a chosen
runtime. One environment can have many: pr-42, pr-43, alice, demo — all from
the same definition, all at the same time, even on the same Docker host.
The one environment that cannot have instances is a blueprint, which exists to be cloned rather than run; clone it and deploy the clone.
TeamEnterprise
Free runs five at once per runtime; redeploying one that is already running never counts against that. See Licensing.
Isolation
Every instance gets a namespace, {project}-{environment}-{instance}, and everything it
creates is prefixed with it: container names, the bridge network, named volumes. That is what lets
pr-42 and pr-43 of the same environment coexist on one daemon without colliding, and what makes
teardown exact — stopping an instance removes what carries its labels and nothing else.
The instance name you type becomes a slug. If that slug is taken within the environment, fadebox
disambiguates it (pr-123 → pr-123-2) rather than failing — convenient in the UI, worth knowing
about in a pipeline, where the CLI's instance up does find-or-create instead
so a retried job does not accumulate copies.
One hostname, one instance. A service reachable through ingress is
published at {namespace}-{service}.{domain}, and no two live instances may answer on the same
name — including instances in different projects, since dashes make two different namespaces
capable of joining to one hostname. A deploy that would claim a name another instance already
serves is refused:
HTTP 409
{
"error": "ingress_host_taken",
"message": "Ingress host 'shop-dev-pr42-web.envs.example.com' is already served by instance 'shop-dev-pr42' — rename the service or the instance, or stop that instance first",
"args": { "host": "shop-dev-pr42-web.envs.example.com", "heldBy": "shop-dev-pr42" }
}
heldBy is the holder's namespace, which reads as {project}-{environment}-{instance}, so it
says which instance to stop — or, if it is not yours to stop, what to rename. An instance holds
its hostnames from the moment a deploy is claimed until it is stopped: one that failed keeps
them, because its containers may still be running and still answering.
Lifecycle
DRAFT ──► STOPPED ──► STARTING ──► RUNNING ──► STOPPING ──► STOPPED
│ │ │
│ (cancel) └────────► ERROR ◄───────┘
└─────────► STOPPING
| State | Meaning |
|---|---|
DRAFT | Being configured; not yet deployed. |
STOPPED | Defined, nothing running. |
STARTING | A deploy is in progress. |
RUNNING | Every service is up and ready — healthy where it declares a healthcheck, merely started where it does not. |
STOPPING | Tear-down in progress. |
ERROR | The last deploy or stop attempt failed. |
Deploy is asynchronous. The call returns immediately with STARTING and the work continues on
a worker; poll the status endpoint (or watch the UI) to see it settle. Stopping mid-deploy
cancels it rather than being refused — STARTING → STOPPING is a supported edge.
Only DRAFT and STOPPED instances can be deleted, or moved to another runtime. An
ERROR instance is both deployable and stoppable, so stop-then-delete is always available; you
can never be stranded.
Deploying an instance that is already running replaces it whole. Every container of the previous deploy is removed first — helpers included, and services that have since been renamed or removed from the environment along with them — and only then does the new generation start. There is no rolling replacement, so the instance is down for the length of the deploy, image pulls included. Named volumes and the instance network survive it, and the URLs do not change. (Stopping removes the containers and the network but keeps the volumes; the only thing that removes an instance's volumes is Reset below, so an ordinary stop or delete leaves them on the host — see reclaiming host resources.) Values resolve before anything is torn down, so a deploy that fails while reading a git value leaves what is running untouched.
Runtime
An instance runs on one runtime, chosen when it is created and shown on its card.
The Runtime field is always offered, however many runtimes the project has, because the set
changes: a runtime can be disabled tomorrow, and an instance created against it stays where it is.
The field is pre-filled with what the server would pick for a request that names none — local
when it is enabled and available to the project, otherwise the project's only enabled runtime —
and left empty when several are enabled and none is local: that case is a real choice, since the
runtime decides the instance's URLs and which daemon holds its volumes, so the server refuses to
guess (409 runtime_unavailable, naming the candidates) rather than pick one.
A disabled runtime blocks deploys, not the instance. An instance whose runtime has been
switched off shows it on its card, and Deploy and Reset are greyed out with the reason. The
way out is Move: a deployer can move a DRAFT or STOPPED instance to any runtime that is
enabled and available to the project — from the card, or fadebox instance move <slug> --runtime <runtime>. The same two states that allow a delete, for the same reason: the instance's row is
the only record of which containers, networks and volumes are its, so it cannot be re-pointed
while any of them exist. Stop it first. The move is recorded in the
audit log as instance.runtime.changed.
A move is not a migration. The instance keeps its slug, its name, its overrides and its history — not its data. Volumes belong to the daemon they were created on and nothing copies them, and each runtime carries its own ingress domain, so the next deploy comes up on new URLs and empty. What the old runtime still holds shows up under its Resources on host as orphans, for an admin to reclaim.
Overrides: what a deployer may change
By default an instance is a name and a runtime, and everything else comes from the definition. An environment can open individual services for variables, for the image tag, or both — and on those services, and only those, whoever deploys sets their own values.
fadebox instance create pr-1842 \
--tag backend=2.7.0-rc3 \
--var backend:SPRING_PROFILES_ACTIVE=test \
--var postgres:SEED_DATASET=expired
Set them at creation or afterwards; either way they take effect on the next deploy, not on
the spot. fadebox instance set <slug> --redeploy — and Save and redeploy in the UI — is the
one step that does both, because editing a log level and redeploying is the loop this exists for.
Where an override lands. A variable override applies to every container of that service's
unit, helpers included — a SEED_DATASET is usually consumed by the init container that seeds
the database, and an override is a knob of the service, not of one container. That is
deliberately wider than the environment's own service variables, which reach the main container
only. Full precedence, lowest to highest:
| Variables | Image tag | |
|---|---|---|
| 1 | Environment's shared variables | Template |
| 2 | Template's own environment: | Environment service row |
| 3 | Environment service row (main container only) | This instance's override |
| 4 | This instance's override (whole unit) |
Values are literal. A {{git:…}} reference is refused — values are literal — so an
override can never point the git resolver somewhere of the deployer's choosing. "Latest test
build" stays what it already is, a reference on the service row; a deployer who wants it leaves
the tag alone. A tag override is held to the same grammar as the row's, so it can only ever
select another tag of the template's own repository.
Refusals are 400s that name the service: setting a variable on a service that is not open comes
back as backend does not accept variable overrides. Variable names match ^[A-Z_][A-Z0-9_]*$,
at most 50 per service, values single-line and at most 200 characters.
Overrides are visible, so they are not for secrets. Every project viewer sees them on the
instance card and in the audit log detail. A value that must not be
read belongs on the environment, where a maintainer keeps it.
If a service is closed again, or removed, the values you set stay stored but stop being applied, and the card marks them no longer applied rather than hiding them — an instance that silently dropped an override would be claiming to run something it does not. Reopen the service and they apply again.
Reset
Reset removes everything the instance owns — containers, network and volumes — and deploys it again with the same overrides. The instance keeps its slug, its namespace, its URLs and its expiry policy; what it loses is state.
That is the difference from a redeploy, which keeps the volumes. Reset is the answer to let me start over: the database goes back to whatever the deploy puts there, and the URL you shared still works.
Available to a deployer from RUNNING, ERROR and STOPPED, and behind a confirmation in the
UI (--yes in the CLI, which a non-interactive shell must pass). There is no separate status —
the instance is STARTING while it resets, and its history reads reset requested → deploy
finished.
Volumes that are not the instance's are never touched: a bind mount or a host path belongs to whoever set it up.
Deploy history
Every deploy and reset writes a row saying what it produced: when it started and finished, whether
it was a deploy or a reset, how it ended, the image each service actually ran after every
resolution pass, and the override values applied. The status panel lists them under the
containers, newest first; GET …/instances/{slug}/deploys is the same list for any project
viewer.
This is what makes an instance card truthful once a tag is a moving git reference: latest test build says nothing a week later, while the record says which image that was on the day. The last 20 deploys per instance are kept.
Expiry
Instances are meant to be disposable, so they carry a lifetime. Every deploy stamps the instance with an expiry — now plus the environment's TTL, seven days unless the installation says otherwise — and a sweep stops whatever has run out.
Expiry stops; it never deletes. A swept instance lands in STOPPED with its definition
untouched, and a redeploy brings it back and starts the clock again. This collects abandoned
containers, not anyone's work.
Two things reset the clock: every deploy and redeploy, so an instance in active use never
expires under its user, and Extend — a button on the instance that pushes the expiry to a
full TTL from now without redeploying. Extend needs the deployer role and appears only while
the instance is running or in ERROR and actually has an expiry. Creating an instance does not
start the clock: a DRAFT has none until its first deploy.
RUNNING and ERROR instances are swept — ERROR deliberately, since a failed deploy leaves
containers holding a network, which is exactly the litter this exists to collect. A deploy in
flight is never swept, and an Extend or redeploy that lands while the sweep is deciding wins.
The sweep runs every minute by default, so a stop follows the expiry closely rather than to the
second.
The instance shows its remaining time as a countdown (3d 4h, then now) with the exact
timestamp in the tooltip; one that ran out and was stopped is badged expired, and
fadebox instance get prints the same as Expires:. Whether a person or the sweep stopped
something is answered by the instance's history, where a swept stop is recorded by the
instance-expiry system actor — see the audit log.
The lifetime itself belongs to the environment: see instance lifetime.
Status is what fadebox did; health is what the daemon says
An instance carries two signals, and they answer different questions.
Status is the lifecycle above — RUNNING, ERROR, and the rest. It records what fadebox last
did to the instance and whether it finished: RUNNING means a deploy brought every service up
and saw it ready. Fadebox does not then keep watching. Once a deploy lands RUNNING, that stays
until you stop or redeploy — whatever later happens to the containers.
Health is what the Docker daemon says about those containers now. Fadebox keeps no shadow
copy of what is running — per-container state comes from the daemon through the instance's labels —
so a container stopped by hand, crashed, OOM-killed or gone unhealthy shows up in the health even
while the status still reads RUNNING.
| Health | Meaning |
|---|---|
HEALTHY | Every container is running — and healthy, where it has a healthcheck; every run-once job has exited 0. |
DEGRADED | Something is up and something is not: a service crashed while its siblings run, or a healthcheck is failing. |
DOWN | Nothing is running — every container exited, or none are left. |
UNKNOWN | Fadebox has not looked yet, or the runtime is unreachable. |
On the instance card the status is the badge; the health is the coloured dot beside it — teal
healthy, amber degraded, red down, grey unknown — and an unhealthy instance shows the reason
(web exited (137)) and when it was last looked at. Health is shown only for a RUNNING or
ERROR instance; a stopped one has no containers to have an opinion about.
Fadebox keeps health current mostly on its own: it watches the daemon's event stream, so a
container that dies, crashes or turns unhealthy is usually reflected within a second or two. A
periodic sweep (every five minutes by default) backs the stream up — it catches what an event
can't tell you, such as a container that died while fadebox was down — and opening an instance's
status panel refreshes it on the spot. An admin can force a whole runtime from Administration →
Runtimes → the runtime → Resources on host → Refresh
instances. A change of verdict is recorded in the audit log as
instance.health_changed.
Health is observed, never acted on: fadebox tells you a container is down, it does not restart
it — that is what a Compose restart: policy is for. To bring a
broken instance back, redeploy it (rebuilds from the definition) or stop it.
Reading the daemon rather than a shadow copy has two consequences worth relying on:
- Running instances survive a fadebox restart. Containers are not tied to the app's lifetime;
after a restart the health reads
UNKNOWNuntil the next refresh, then catches up. - What the status panel shows is what exists. Per-container state, health and logs come from the daemon at the moment you ask.
If the app is killed mid-deploy, the instance is left claiming a worker that no longer exists. On
the next startup such instances are swept to ERROR — deliberately not to a state claiming to know
what is on the host, because that question is answered by the health.
What a container is costing
Each container on an instance's status shows the CPU it is using and its memory against its limit
— 0.42 cores · 312 MiB / 2 GiB. A service that set no memory limit shows the host's total as its
ceiling, so the figure reads as used of host rather than of a limit it does not have. Below the
container list, Volumes is what this instance's managed volumes take on the host, which is the
number to look at when a runtime's disk is filling.
These come from the same periodic reading as the runtime's own bars, not from a fresh call to Docker for this page — which is why the status view can keep refreshing without asking the daemon anything. The same rules apply: a dash means nothing has been observed yet rather than nothing is being used, CPU needs two readings before it says anything, and the volume figure comes from the slower disk pass.
Logs
The status panel shows a per-service tail — a snapshot of one service's output (capped at 5000 lines), not a stream. The UI and the CLI both read the same endpoint. Both read what the daemon still holds — Container logs is the cap that decides how far back that is.
Next to the tail sits Download, which streams the whole instance's logs as one zip: a
logs/<service>.log entry per container — init helpers and sidecars included — plus a
manifest.json recording what was exported (image, state, exit code, bytes, and whether the entry
was cut short, per container). Lines carry the daemon's timestamps, so output from different
containers can be ordered.
The download URL takes scoping parameters, which is also how a pipeline uses it with an API key:
curl -sf -OJ -H "Authorization: Bearer $FADEBOX_API_KEY" \
"$HOST/api/projects/$P/environments/$E/instances/$I/logs/export?since=2026-08-16T12:00:00Z"
| Parameter | Default | Meaning |
|---|---|---|
service | all containers | Repeatable; takes the service keys the status panel shows. |
tail | 100000 | Last N lines per container, or all. |
since / until | (unset) | ISO-8601 instants; the Docker API applies them at second granularity. |
timestamps | true | The per-line timestamp prefix. |
Docker cannot report a log's size in advance, so the export is bounded rather than measured:
the default tail keeps each container's recent end, and byte caps
(configurable) cut a pathological entry mid-stream
with a visible [fadebox] truncated … marker rather than failing the download — the zip is
always valid, and the manifest says exactly what was cut.
Both the tail and the export read live containers, and container logs die with the containers —
on a stop, and on the redeploy that replaces them. Export before you stop or redeploy
something you are still diagnosing. Any project viewer may read
logs and download the export; each download is recorded in the
audit log.
Ownership
The creator of an instance is recorded and displayed, but it is not an access control: any
deployer in the project may deploy, stop or delete any instance in it. That is what lets a
pipeline's cleanup job tear down what a different job started.