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. |
PROVISIONING | Only on an elastic runtime: the deploy has been accepted and a machine is being made for it. Nothing is deployed yet, and stopping cancels. |
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.
On an elastic runtime the claim lands one step earlier, in
PROVISIONING, because there is no host yet: a machine has to be created and booted first, which
takes minutes. The deploy then follows by itself and the instance carries on through STARTING as
usual. Stopping a PROVISIONING instance is instant — there are no containers — and deleting one
is refused until it is stopped, as from any other busy status. See
PROVISIONING. While it waits, the card says how far the
machine has got under Provisioning machine… — waiting for its Docker daemon, starting the
ingress router — and the whole story is the machine log.
Such an instance also names the size its machine was built at beside its runtime, and carries a machine chip on the line below saying what the machine is doing while it is anything but up — creating machine, machine booting, machine stopped · disk kept. Three of its readings are amber and are about cost or data rather than a deploy: machine not stopping, kept after a failure, and the countdown on a stopped disk the runtime is about to give up — disk kept 5d 4h more, then disk expired 2d ago once it has. See the instance and its machine.
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.
What the card offers is one action and a menu. The instance's state picks the button —
Deploy on a DRAFT or STOPPED instance, Redeploy on one that is RUNNING with an
edited environment or in ERROR, Stop while it runs, Cancel while it is provisioning or
starting — and everything else is behind More: Deploy or Stop when the state did not lead with
it, Reset, Extend, Describe, Edit overrides, Move, Approve and Reject, sharing, Access, and
Delete instance last. Nothing is hidden from anybody: an action this caller may not use stays
in the menu, greyed out and naming whom to ask. The status panel lays the same set out as a row of
buttons, for the surface that has the width for it.
Deploying an instance that is already running converges it: the services that changed are restarted and the rest keep running — see Redeploy below. A service that is restarted has its containers replaced, so there is no rolling replacement within it. Named volumes and the instance network survive a deploy, 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.
Redeploy restarts only what changed
A redeploy does not tear the stack down and build it again. It compares what is running against what the environment now resolves to, container by container, and touches the difference:
| Outcome | When |
|---|---|
| created | Nothing is running for it — a first deploy, or a service added since the last one. |
| recreated | Its configuration differs from the resolved definition, the image moved under its tag when it was pulled, or Fadebox would not call it healthy at this moment. |
| kept | None of those: same configuration, same image, up and ready. |
| removed | Nothing in the environment names it any more — a service renamed, or dropped. |
So bumping one service's tag restarts that service and leaves the Postgres, the Keycloak and
everything else alone, which is what docker compose up -d does and what people expect a redeploy
to mean.
Three rules decide what comes back up with what:
- A service is a unit. Its primary container, its init helpers and its sidecars restart together — the unit is kept only when every container of it is kept. That is what stops a stale sidecar surviving beside a fresh main container, and it is why an init job re-runs when its own service does rather than on every deploy.
- Nothing cascades. Recreating a wave-0 database does not recreate the wave-1 application that talks to it; a dependent is expected to reconnect, which is what a start order already assumes of it.
- Images are pulled before anything is touched. A tag that does not exist, or a registry login that is missing, fails the deploy with the instance still running — the same way a failed git value read does. Nothing is left half torn down.
Reset is the other behaviour, and it is unchanged: it replaces everything the instance owns, volumes included.
Every attempt records what it did per container, so a row of the deploy history reads restarted backend · kept 4 and the attempt's page says it per service. Containers created by an older Fadebox carry no configuration stamp and are therefore recreated once, on the first redeploy after the upgrade.
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; over
the API or the CLI either one is refused with 409 runtime_unavailable, naming the enabled
runtimes the project could use instead. The way out is Move, under the card's More menu:
a deployer can move a DRAFT or STOPPED instance to any runtime that is enabled and
available to the project — there, 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. The variables field flags a
{{ and a name outside that pattern as you type, before anything is sent.
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 — Reset under the card's
More menu, behind a confirmation (--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.
What a copy is for
An instance knows what it runs. What it cannot work out for itself is why somebody made it —
which pull request, which customer demo, which bug. So an instance carries a purpose, one
line under its name, and up to eight links, each a label and an http/https URL: the merge
request, the ticket, the design doc.
fadebox instance up pr-1842 \
--purpose "Discount codes, PR #842" \
--link "PR #842=https://gitlab.example.com/shop/-/merge_requests/842" \
--link "SHOP-1291=https://jira.example.com/browse/SHOP-1291"
Set them when the instance is created — the New Instance sheet has both fields, and so do
instance create and instance up — or afterwards, from Describe… under the card's More
menu or fadebox instance describe <slug>. Setting them is a deployer act, like the rest of the
instance lifecycle.
They describe the instance, not what it is running, so they survive every redeploy. The pull request is the same pull request after the fourth push, and no deploy, reset or stop clears them. That is why they sit on the instance, while checks and the verdict — statements about a content — sit on the deploy attempt instead.
Writing them replaces both together: a call that sends no purpose clears the purpose, and one
that sends no links clears the links, the same full-replace rule as
overrides. The CLI softens the sharp edge where it can —
give instance describe only --link and it carries the existing purpose over, so adding a link
never wipes the line — and --clear removes both.
A purpose is at most 200 characters on a single line; a link label at most 40. Control characters
and bidirectional overrides are refused (400 purpose_invalid), because fadebox instance list
prints the purpose into a terminal — it has a PURPOSE column between SLUG and STATUS, cut at
40 characters — and a line that renders as something other than what it says is worse than no line.
Checks and the verdict
Two more facts, and they answer is it fit rather than what is it: a check is a pipeline's statement about a suite it ran, and a verdict is a person's yes or no. Both are written on the instance's running deploy attempt — the newest row of Deploy history — and never on the instance itself.
That placement is the whole design. A green end-to-end run and an approved were about the content that was up at the time, so a redeploy starts a new row with no checks and no verdict, and the card says so. Nothing is cleared and nothing is copied: the previous attempt keeps both, which is what answers the tester who says but it passed yesterday.
Checks
A pipeline reports a named result, and repeats it as the run progresses:
fadebox instance check pr-1842 e2e --state pending
# … run the suite …
fadebox instance check pr-1842 e2e --state passed \
--summary "84 passed, 0 failed" --run-url "$CI_JOB_URL"
- The name is a lowercase slug starting with a letter, at most 40 characters —
build,integration,e2e,smoke. Reporting the same name again replaces it, which is exactly what lets one suite saypendingwhen it starts andpassedorfailedwhen it ends. - The state is
pending,passedorfailed. The one-line summary and the link to the run are optional;--run-urlis spelled that way because--urlis already the CLI's own server address. - Who reported it and when are the server's, taken from the credential that made the call and never read out of the request body, so a check cannot be made to name somebody else.
- At most 20 checks on one attempt, and
--removedeletes one.
The verdict
A person says yes or no to what is running, once per deploy attempt:
fadebox instance approve pr-1842 --note "Discount totals correct on all three tiers"
fadebox instance reject pr-1842 --note "Stacked coupons still give the wrong total"
A note is optional on an approval and required on a rejection — a no without a reason is the
thing the ticket comment would have said anyway, and the point of writing it here is that it sits
beside the content it is about. Up to 500 characters, and it may span lines. A second verdict
replaces the first, with the audit log keeping both;
fadebox instance verdict <slug> --withdraw takes it back, and the same command with no flag
prints what stands.
Approving and rejecting are deployer acts — the rung a tester and a product owner
already hold, which is the rung that brings a
stack up and says whether it is right.
A verdict is not a lifecycle state. It does not move the instance's status or health, and it is not a reason to keep an instance alive: an approved instance expires like any other. If it should outlive its TTL, extend it.
When a check or a verdict is refused
Both are accepted only while the instance is RUNNING and its newest deploy attempt ended
RUNNING. Otherwise the answer is 409 instance_state — it has no running deploy to check.
That closes the window a redeploy opens: a check arriving while the stack is being replaced would
otherwise be recorded against content that is already on its way out, and failing the pipeline
step is the better of the two. A malformed check is 400 check_invalid, a malformed verdict
400 verdict_invalid.
Where they show up
On the instance card, checks are badges and the verdict a chip beside them (Approved by alice),
and Approve and Reject are the first two items of the card's More menu while the
instance is RUNNING. The status panel has a Checks section with each summary and a link to
the run, and a Verdict block with the note and who left it. Every row of the
deploy history carries the checks and the verdict of that attempt, and the
attempt's page shows them in full — which is the history the placement rule exists to give you.
An agent can set a verdict (set_instance_verdict) but cannot report a check. That is deliberate,
and the reasoning is on the MCP page.
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, the override values applied, what the attempt did to each container — created,
recreated, kept or removed — and, once it is up, the
checks and the verdict left on it. The last 20 attempts per instance
are kept, and GET …/instances/{slug}/deploys lists them, newest first, 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 instance's history
The card's history chip — and Full history in the status panel, whose Deploys section
keeps one line per attempt — opens the instance's history: one row per kept attempt, newest
first. Under each row is what changed since the attempt before it — web :1.4.0 → :1.4.2,
web LOG_LEVEL debug → info, gitops@master a3f91c2 → 9b1e0d4 — then what the attempt did to
the containers (restarted web · kept 3), and the checks and verdict of that attempt. Pick two
rows and Compare opens the newer against the older.
One attempt, whole
Every row is a page. It joins, per service, the four things the record keeps about it: the image that ran, the repository, ref and commit it came from, the override values as they were applied — not as the instance has them now, which a deployer may have edited since — and whether its container was created, restarted, kept or removed. Above the services: the error as it was recorded, each check with its summary and link, the verdict with its whole note, and the download of the log its containers left when a stop or reset removed them.
Changed since leads the page. By default it is the difference against the previous kept
attempt, whatever that one's outcome — after a failure, what did I change that broke it is the
question. The picker offers every other kept attempt, and the choice goes into the URL
(…/deploys/201?against=151), so a comparison can be pasted into a ticket. Compared are the
images, the override tags and variables, and the commit each git reference resolved to; a service
that arrived or left says just that. Container outcomes, checks and verdicts are not compared —
they are statements about an attempt, not state it carried. The difference is computed from the
two records when the page is shown; nothing is stored.
The environment's history
History on the environment page lists every kept attempt of every instance the environment
has, newest first and paged, with the same columns — instance, when, deploy or reset, outcome and
duration, images, containers, checks — and filters by instance and by outcome, where in
progress selects the attempts that have no outcome yet. Each row opens its attempt. Over the API,
GET …/environments/{envName}/deploys takes page, size and sort (startedAt, instance,
outcome, kind), a repeatable instance (slugs) and a repeatable outcome (RUNNING,
ERROR, CANCELLED, or PENDING for in flight; anything else is a 400), and answers
{instance: {slug, name}, attempt: …} per row, the attempt in the same shape as the instance's
own listing minus the log capture. A deleted instance's attempts go with it; the
audit log is what remembers a deleted thing.
Which commit a git reference read
Every {{git:…}} reference the environment uses resolves to a
commit when the instance deploys, and the attempt records what it resolved to: per repository and
ref, the commit it answered from and which services read it. So the card reads gitops · a7c48fe
on master, and last Tuesday's attempt still names the commit it ran, long after the branch has
moved on.
There is nothing to set and nothing extra is fetched — the deploy already had the answer. This is the half of why this copy exists that can be derived, which is exactly why purpose and links are for what git cannot know: the pull request, the ticket, the customer.
The commit is a link to the forge's commit page when the repository's remote is on a host
whose URL shape is known — github.com, gitlab.com or codeberg.org, over https. Everywhere
else — a self-hosted GitLab, a Gitea, an ssh:// remote — the text stands on its own. A
self-hosted forge cannot be told from any other host by looking at its URL, and a link that lands
somewhere wrong is worse than no link.
The record stores the repository's slug, never its remote URL, and the link is rebuilt from
the remote's host and path at the moment the page is rendered. A credential written into a remote
URL therefore never reaches the record or the page, and a viewer who may read the deploy history
does not learn from it where the remote lives.
fadebox instance get prints the same under References:, and fadebox instance status as a
table of repository, ref, commit and the services that read it.
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 — an item under the card's More menu 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.
On an elastic runtime the same thing happens against the instance's own
machine: the sweep and the event stream both reach each machine separately, so an instance there
is read as closely as one on a shared host. An instance whose machine is not ready shows UNKNOWN
and says which case it is — it has no machine yet, or the machine is in some state other than
ready — instead of leaving an old verdict standing for a host that is not answering. Opening its
status panel reads the same UNKNOWN: with no daemon to ask there is nothing to call DOWN, so an
instance whose failed deploy switched its machine off is not shown as down, and its audit log
gains no instance.health_changed rows from the panel and the sweep disagreeing.
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.
An instance on an elastic runtime shows the same per-container figures — the reading reaches each machine separately, and a container carries the same labels there as on a host. Volumes is the exception: the slower disk pass does not visit machines, so it reads as a dash, as do the runtime's own bars, which describe a host such a runtime does not have.
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.
Any project viewer may read logs and download the export; each download is recorded in the
audit log.
On an elastic runtime the status panel's log pane also lists an entry named machine: what
Fadebox said while it made, started or stopped the instance's machine — the
machine log. It is readable while the instance
is PROVISIONING, when there are no containers yet, and it is not part of the download.
Logs outlive a stop
A stop and a reset remove the instance's containers, and a container's log would go with them — worst for exactly the instance that failed, was stopped to free the runtime, and is now the subject of what did it actually say. So both read every container's log before removing it and keep the capture beside the deploy attempt it belongs to: one file per service, and a manifest saying what was captured and what was not.
Nothing about reading changes. When the instance has no running container, the same tail and the same Download serve the capture instead, and say so: the status panel marks the Logs section captured at the stop's time, and over the API two response headers name the source —
X-Fadebox-Log-Source: live | capture
X-Fadebox-Log-Captured-At: 2026-09-14T09:12:03Z
— so the body stays the log verbatim, with nothing Fadebox invented in it. The CLI's
fadebox instance logs prints a one-line note on stderr when it shows a capture, and its stdout is
still only the log. A downloaded capture carries "source": "capture", the attempt and the time in
its manifest.json.
Each attempt keeps its own capture, so an attempt's page in the deploy history offers a download of the log its containers left even after later attempts, from
GET …/instances/{slug}/deploys/{id}/logs/export
A capture is scoped and capped exactly as an export is — the default tail, the same byte caps and
the same truncation marker — which is also why the export's scoping parameters (tail, since,
until, timestamps) are refused with a 400 on a capture: the file is already scoped, and
re-scoping it silently would make one parameter mean two things. service still filters. A
capture is bounded in time as well as bytes, so a runtime that stops answering delays the stop by
at most the read timeout and costs that service its
capture, never the stop itself; a capture that could not be written is recorded in the manifest
and the stop completes regardless.
What is not captured: a redeploy. It removes containers too, but what it leaves behind is a running instance whose live logs work, so export first if you are still diagnosing the service a redeploy is about to replace. Captures live on the server's disk under a configurable directory, kept as long as their deploy attempt — the last 20 per instance — and removed with the instance, its environment or its project. Losing that directory is harmless: every reader simply says there is no capture.
Who can open its URLs
An instance's Access says who may open its ingress URLs: Open — anyone who can reach
them; Members — the project's members and administrators, after a Fadebox sign-in; or
Creator — its creator while a member of the project, the project's editors and
administrators. The level is the instance's own: Access under the card's More menu,
fadebox instance gate / ungate, or gate on the create call. What a new instance starts at is
its environment's default. Raising it is for whoever
may operate the instance; lowering it takes editor.
Opening or closing the gate applies on the next deploy, and until then the card says what is
actually in force before what was asked for: open, members on next deploy means the URLs are
still open. A move between Members and Creator applies to the next sign-in at once. The whole
flow — what a visitor sees, how a pipeline gets through, what the installation must have set up —
is under Requiring a sign-in. Every change writes
instance.gate_changed to the audit log, with the level it ended on.
This is a separate question from ownership below, which is about who may operate the instance, and from a protected environment, which is about who may edit the definition.
Reaching its services from your own tools
An instance of an environment with native access on carries a
gateway into the installation's tailnet, and each of its services a fixed address there — so
psql, DBeaver or redis-cli on a developer's laptop connect to 10.224.0.3:5432 directly, with
no port published on the host. The drawer's Connect section shows the gateway's state and a
row per service with its address, ports and a copy button; fadebox instance connect prints the
same. The route is the tailnet's policy to grant: the instance's access level and its sharing do
not apply on the tailnet.
Without a tailnet, the instance agent
Fadebox runs in every instance does the same through Fadebox itself: fadebox connect pr-42
tunnels each service to localhost on your machine, and here the instance's access level does
apply — every connection is checked against the project role and the gate, and audited.
Ownership
Every instance names who created it — by alice on the card, with a mail link when the installation shows email addresses (below). What that name lets anyone do depends on whether the instance is shared or personal.
A shared instance is the project's: any deployer may deploy, stop, extend, reset, move or
delete it, set its overrides, describe it and report on it. That is what lets a pipeline's
cleanup job tear down what a different job started. A personal instance is its creator's:
only they, the project's editors and maintainers, and a global admin may do those things.
Everyone else in the project still sees it — the card, its status, its logs, its history — and
the controls they may not use are disabled with the reason, naming whom to ask. The API refuses
the same calls with 403 instance_personal, carrying the creator's username.
Which one an instance starts as is the environment's call: each environment has a New
instances are shared switch, described under
shared or personal instances. A new environment
starts with it off, so its instances are personal unless the editor says otherwise; every
environment that existed before this feature keeps producing shared ones, so nothing changed on
upgrade. Whoever creates an instance can choose the other on the create form (--shared or
--personal in the CLI), and the creator or an editor can
share or unshare it later, in either direction — Share with the project and Make personal
under the card's More menu. Other deployers cannot, whichever way it
currently points. Every change writes instance.sharing_changed to the
audit log.
The line was drawn at editor rather than maintainer on purpose: whoever composes the
environment operates everything running from it. A trainer resets any attendee's lab, and the
developers stop any instance CI made — which matters, because a service account is a creator
like anyone else. A per-pull-request pipeline's instances are personal to that account unless
its environment says new instances are shared, so a cleanup job that runs as a different
account needs editor, or the switch on the environment. The expiry sweep stops a
personal instance like any other.
If the creator's account is removed, a personal instance is left to the editors, who can share it with one click; nobody inherits it by accident.
The creator's email address. By default the name on the card links to the creator's email,
so a colleague blocked by a personal instance can reach them. An administrator can switch that
off under Administration → System (Show instance creators' email addresses) for an
installation that runs across contractors or partners. The name stays on the card either way,
and the switch writes system.creator_email_changed to the audit log.