The fadebox CLI
fadebox is a single native binary that drives the same API the UI does, authenticating with an
API key. It was built for pipelines — the useful half is not wrapping REST calls,
it is the deploy loop, since deploy is asynchronous and anything that gates a build on an
environment coming up has to poll, decide what counts as success, and report why it failed.
It works just as well from your own machine. Any key drives it, so you can point it at a key of your own and act as yourself, or at a service account's key and act as the pipeline does. If you sign in through SSO you have no password, so a personal key is how you use the CLI at all.
If what you want is an agent driving Fadebox rather than a command you type, that is the MCP endpoint instead.
Install
Every Fadebox server hands out the CLI it was released with, so the download is your own installation, not a release page. It comes in six builds, for Linux, macOS and Windows, each on amd64 and arm64:
# Linux on amd64; on ARM, fadebox-linux-arm64
curl -fsSL -o fadebox https://fadebox.example.com/cli/fadebox-linux-amd64
chmod +x fadebox
sudo mv fadebox /usr/local/bin/fadebox
# macOS on Apple silicon; on an Intel Mac, fadebox-darwin-amd64
curl -fsSL -o fadebox https://fadebox.example.com/cli/fadebox-darwin-arm64
chmod +x fadebox
sudo mv fadebox /usr/local/bin/fadebox
# Windows on amd64; on ARM, fadebox-windows-arm64.exe
curl.exe -fsSL -o fadebox.exe https://fadebox.example.com/cli/fadebox-windows-amd64.exe
Then put fadebox.exe in a folder on your PATH.
The download needs no sign-in — a pipeline's first step has nothing but the server's address, and
that is enough — and it is always the server's own version: after an upgrade,
download it again and fadebox --version matches the version in the sidebar footer. That footer
links it too, on every page: CLI beside the version is the build for your machine, and the
arrow next to it lists all six. The browser does not report a Mac's processor, so the footer
offers a Mac the Apple silicon build; on an Intel Mac, pick macOS (amd64) from the list. An
iPad, which calls itself a Mac, and 32-bit ARM get no guess, only the list. The binaries'
third-party notices sit beside them at /cli/fadebox-THIRD-PARTY-NOTICES.txt — the 1.4 name,
/cli/fadebox-cli-THIRD-PARTY-NOTICES.txt, answers with the same file — and the License page
links them with the server's own.
It is a single static binary with no runtime dependency — nothing to install alongside it. A Fadebox built from source carries no CLI and says so at that URL; a published image always does.
Use the CLI of your server's own version. An older CLI keeps working against a newer server only
as far as it knows the values that server sends. From 1.4 on, the CLI reads an instance state it
does not know as unknown instead of failing: the table shows unknown, and --wait keeps
waiting through it until a state it knows or its deadline, so a later server can add a state
without breaking a pipeline. A 1.3 CLI cannot follow instances on an
elastic runtime: a 1.4 server reports them as PROVISIONING
while their machine is made, and the 1.3 binary fails on that value — instance up and deploy
exit non-zero on a deploy the server accepted, instance list fails while any instance on the
page is provisioning, and get, status and --wait fail the same way. Downloading the CLI
again from the upgraded server is the fix.
Connect
login verifies a key against a server and stores it as a context:
fadebox login --url https://fadebox.example.com --project demo --env dev
It prompts for the key so it does not land in your shell history, calls /api/me to confirm it
works, and writes ~/.config/fadebox/config.yaml — created with mode 0600 before anything is
written to it, so the token never touches disk world-readable. Give the server's own address:
the CLI follows no redirect, from http:// to https:// or anywhere else, and refuses one
naming where it points.
Whatever login prints back is who the CLI now acts as: your own username for a personal key, the
service account's for a CI one. Every command afterwards reaches exactly as far as that account
does.
Contexts are named, so one machine can talk to several installations:
fadebox context list
fadebox context use staging
fadebox context current
In CI, write no file at all. Every setting resolves flag → environment variable → context file:
| Flag | Environment variable |
|---|---|
--url | FADEBOX_URL |
--token | FADEBOX_TOKEN |
-p, --project | FADEBOX_PROJECT |
-e, --env | FADEBOX_ENV |
FADEBOX_CONFIG_DIR relocates the context file if you do want one. Without it the file is in
$XDG_CONFIG_HOME/fadebox, else in $HOME/.config/fadebox — $HOME as the shell has it, so
under sudo -E or in a container with a HOME of its own, set FADEBOX_CONFIG_DIR to share one
file. A config.yaml that is a symbolic link, into a dotfiles checkout say, is saved through the
link. To hand the file to a container, mount its directory rather than the file alone: a save
replaces the file, which a single-file bind mount does not allow.
Proxies. The CLI honours HTTPS_PROXY, HTTP_PROXY and NO_PROXY (in lower case too), for
every command and for connect's tunnel, as curl does. A runner whose proxy is for the internet
only lists the Fadebox server in NO_PROXY. Through a proxy an https server stays encrypted end
to end; a plain http one is readable by the proxy.
The deploy loop
fadebox instance up pr-123 --wait --timeout 10m
upis find-or-create. If an instance with that slug exists it is reused and redeployed; otherwise it is created first.instance createstays strict by comparison — the API disambiguates a repeated name intopr-123-2rather than failing, which is right for a UI and a quiet duplicate-per-retry for a pipeline.--waitpolls status and renders per-container state and health as they come up. It exits0onRUNNINGand1onERROR, printing the failure reason the UI would show — otherwise only reachable by having a human open the UI.RUNNINGis as strong as the template's healthchecks: a service that declares one counts as ready when it turns healthy, one without the moment it starts — so on a healthcheck-less stack--waitcan return before the app accepts a connection.--timeout(default10m) and--poll-interval(default2s) bound the wait. They accept90s,10mand the like. A poll that fails on the way — a server slow to answer, a dropped connection — is polled again; only the timeout orCtrl-Cends the wait early.--waitwaits throughPROVISIONINGtoo. On an elastic runtime a deploy first has to have a machine made for it, so the instance sits inPROVISIONINGfor minutes before it even reachesSTARTING. That status is not terminal and--waitkeeps polling, but a cold start can outlast the default ten-minute timeout — raise it on such a runtime rather than reading the timeout as a failure. Meanwhile--waitprints the machine log as it grows, each line once, asmachine: <line>— from this deploy's Machine requested line to Deploying, and the reason if the machine fails — so the job log shows what the minutes were spent on.deploy --waitandreset --waitdo the same.stop --waitneeds no such timeout: a stop on aPROVISIONINGinstance is a cancel, and it lands onSTOPPEDat once.- On success it prints the instance's URLs, taken from the deployed containers.
--runtimeapplies on create only — an existing instance keeps its runtime untilinstance movechanges it, which needs the instanceDRAFTorSTOPPED. Left out, the server pickslocalwhen it is enabled and available to the project, otherwise the project's only enabled runtime, and answers409 runtime_unavailablewith the candidates when several qualify — name one then.instance listandgetmark a runtime that has since been disabled as(disabled): the instance cannot deploy there, see Runtime.- Deploying re-arms the instance's lifetime. A branch that keeps getting pushed keeps its
environment alive; one that goes quiet is stopped once the
expiry runs out, and
instance getprints when that is. instance listhas aHEALTHcolumn besideSTATUS, andinstance getprints aHealthline, carrying what the daemon last said — status is what fadebox did, health is what is running now. A pipeline can gate on it:RUNNINGmeans the deploy finished,HEALTHYmeans it is still up.--purposeand--linksay what the copy is for — the branch, the merge request, the ticket — so the instance list reads as something other than a wall of slugs. Both work oncreateandup,--linkis repeatable, and they stick to the instance across every redeploy.instance listshows the purpose in aPURPOSEcolumn, cut at 40 characters.--sharedand--personalsay whose the copy is: every deployer's, or the creator's and the editors' alone. Both work oncreateandup, on create only — an existing instance keeps its sharing, andupsays so when the flag disagrees — and left out, the environment's own default applies.instance shareandinstance unsharechange it afterwards, which is the creator's or an editor's to do.instance listshows it in aSHARINGcolumn andinstance getprintsShared: no (personal to alice); astopordeletethe account may not make prints the server's refusal, which names whom to ask.--gate open|members|creatorsays who may open the instance's URLs — its access level; left out, the environment's own default applies. It works oncreateandup— and unlike the sharing flags,upapplies it to an instance that already exists, before it deploys: a pipeline that re-runsup --gate membersmust never end up with open URLs and a zero exit. If the level is refused,upfails without deploying. A level below the one the instance has, or on create below the environment's default, takeseditor.--ungatedis--gate open;--gatedasks for at least members, so an instance an editor has closed to its creator stays that way.instance gate(members, or its creator with--creator) andinstance ungatechange it afterwards; plaininstance gatenever lowers a creator gate either. Opening or closing the gate takes effect on the next deploy, a move between members and creator on the next sign-in.instance listshows the level in anACCESScolumn —open,members,creator, oropen → memberswhile a change waits for its deploy — andinstance getprints aGate:line that says the same.--print-tokenmints the ingress cookie for an instance that requires a sign-in, so the suite can open the URL the deploy just printed. It implies--waitand adds anaccessTokenkey to the status document; for an instance deployed open the key is there with an empty token list, so the step needs no condition around it.
What an instance runs is its environment's definition, read fresh on every deploy — an instance
carries no parameters of its own. To deploy other versions, set them on the environment first
(env service set), or clone the environment into a second
version stream (env clone).
A blueprint environment refuses instances altogether —
instance create and up answer 409. env list marks one in its KIND column and env get
prints a Blueprint: line, so a pipeline that must not assume can check before it deploys. Cloning
a blueprint yields a deployable environment, so env clone base dev then instance up is the way
through.
--wait is available on up, deploy and stop.
Command surface
fadebox login | Verify a key and store a context. |
fadebox context list | use | current | Switch between installations. |
fadebox env list | get | Environments of the project. |
fadebox env clone <source> <slug> | Copy an environment's definition — the second version stream, and how a blueprint is used. |
fadebox env service set <name> --image-tag … | Set what a service runs: image tag, env overrides, job flag. A full replace — options left out are cleared (except --job). |
fadebox instance list | get | create | up | deploy | stop | delete | The instance lifecycle. |
fadebox instance move <slug> --runtime <runtime> | Move a DRAFT or STOPPED instance to another runtime. Its volumes do not follow and the new runtime has its own ingress domain, so the next deploy comes up on new URLs and empty — see Runtime. |
fadebox instance reset <slug> --yes | Wipe the instance's data — containers, network and volumes — and deploy it again with the same overrides. --yes is required when stdin is not a terminal. |
fadebox instance set <slug> --var … --tag … [--redeploy] | Set this instance's values on the environment's open services. A full replace: send every override it should end up with, or --clear to remove them all. |
fadebox instance describe <slug> --purpose … --link … | Say what this copy is for: one line and up to eight labelled links. Pass only one of the two and the other is carried over; --clear removes both. |
fadebox instance share | unshare <slug> | Make an instance every deployer's, or its creator's alone — see Ownership. The creator's or an editor's to do. |
fadebox instance check <slug> <name> --state passed|failed|pending | Report a named check on what the instance is running now — --summary for the line, --run-url for the job, --remove to delete it. |
fadebox instance approve | reject <slug> --note … | A person's verdict on what is running. A rejection must carry the note. |
fadebox instance verdict <slug> [--withdraw] | Print the verdict that stands, or take it back. |
fadebox instance gate [--creator] | ungate <slug> | Close an instance's URLs to the project's members — or with --creator to its creator, the project's editors and admins — or open them again; see Requiring a sign-in. Opening or closing applies on the next deploy; lowering the level takes editor. |
fadebox instance token <slug> | Mint the ingress cookie for an instance that requires a sign-in — one value per hostname, valid an hour or until the instance expires. An instance deployed open answers with none; one gated to its creator refuses anyone but its creator and the project's editors. |
fadebox connect <slug> [service[:port]…] | Tunnel the instance's services to localhost through the instance agent, until Ctrl-C — or, when this machine is on the instance's tailnet, print their addresses. --port sets a local port, --tunnel skips the tailnet. |
fadebox instance connect <slug> | How your own tools reach the instance's services: over the tailnet, the gateway's state, each service's fixed address and ports, and whether this machine's Tailscale client accepts subnet routes; then the instance agent's state — see Native access. Reads only; --no-local skips the local client. |
fadebox instance status | logs | Live state and a log snapshot — for a stopped instance, the log captured at the stop, with a note on stderr. |
fadebox template list | get | Templates visible to the key's account. list shows up to 500, and past that says on stderr how many of how many you are looking at. |
fadebox runtime list | Runtimes; narrowed to those the project may deploy onto. CPU and MEMORY show what each host is spending against its capacity, as of the last reading — – where nothing has been observed yet. An elastic runtime has no host of its own, so its HOST reads elastic (gcp via local): the provider, and the front its URLs come from. -o json carries placement for a script to branch on. |
fadebox runtime machines <runtime> | The machines an elastic runtime is holding — and which of them are still billing with nothing left on them. Global admin only; a static runtime answers that it provisions none. |
fadebox runtime machines prints NAME STATE DESIRED PROFILE INSTANCE SINCE ATTENTION ERROR, ordered by name. An empty INSTANCE is the row it exists for: a machine whose instance
was deleted or moved, or whose stopped disk aged out. --state <state> narrows to one machine
state — a state that does not exist is refused with the list of the ones that do — and
--attention to the machines an admin should look at: a failed one, a call the provider keeps
refusing, a disk nobody came back for. ERROR is shortened to one line in the table and carried
whole in -o json. It asks for up to 500 machines, and past that says on stderr how many of
how many you are looking at, so a redirected stdout is still the listing and nothing else. What
the rows mean is in Keeping track of machines.
create and up take the same --var <service>:<KEY>=<value> and --tag <service>=<tag> flags,
both repeatable, so an instance can be made with its values in one call:
fadebox instance up pr-1842 --tag backend=2.7.0-rc3 --var backend:LOGGING_LEVEL_ROOT=DEBUG --wait
Whether a service accepts either is the server's decision, so a closed one fails fast with the
400 it would give anyone. Setting values does not deploy — add --redeploy to set to follow
through.
Every command reaches exactly as far as the key's account does — a 403 means a missing role,
exactly as it would for a person.
A 401 means the server would not accept the token at all. The CLI cannot tell you which reason,
because the server deliberately gives the same answer to every one of them — a wrong secret, a key
that was revoked or has expired, a deactivated account, and an account being made to change its
password all come back identically, so the endpoint cannot be used to probe which keys exist. The
things worth checking, in the order they usually bite: has the key expired (its expiry is on the
key's row in the UI), was it revoked, and can the account still sign in at all.
Output and exit codes
-o table (default), -o json and -o yaml. Every command emits one document, so jq reaches
any single field of a result. A table renders timestamps in your shell's own time zone — the
time-zone database is built into the binary, so a minimal container image without one still
prints local times; json
and yaml carry them as the API does, as instants with the offset spelled out
(2026-08-16T09:30:00Z). A status document keys its containers by service, so one service
is an address rather than a search:
fadebox instance status pr-123 -o json | jq -r '.containers.web.urls[0]' # one service's URL
fadebox instance status pr-123 -o json | jq -r '.containers[].urls[]' # every URL
fadebox instance status pr-123 -o json | jq -r '.containers | keys_unsorted[]' # the services
A service has one URL per ingress port, so urls is a list even for a single service. Beside
containers, a status document carries checks (keyed by check name), verdict and
references — the facts the card shows about
what is running — so a job can read back what it reported without a second call. instance get
prints the same as Purpose:, Links:, Checks:, References: and Verdict: lines.
--wait writes its live container progress — and on an elastic runtime the machine's lines — to
stderr, and only the final result to stdout. So
URL=$(fadebox instance up pr-123 --wait -o json | jq -r '.containers.web.urls[0]') captures the
URL alone while you still watch the deploy happen in the job log.
| Exit code | Meaning |
|---|---|
0 | Success. |
1 | The operation failed — a deploy that ended in ERROR, or any 4xx/5xx. |
2 | Usage error. |
A refusal prints the server's own sentence — "Instance is RUNNING — stop it before deleting" — not the JSON it arrives in or the bare status line; a failed validation adds the fields it names after it.
A pipeline
export FADEBOX_URL=https://fadebox.example.com
export FADEBOX_TOKEN=$CI_FADEBOX_KEY # from your CI secret store
export FADEBOX_PROJECT=demo FADEBOX_ENV=dev
# the CLI comes from the server it is about to drive: the same version, no key needed
curl -fsSL -o /usr/local/bin/fadebox "$FADEBOX_URL/cli/fadebox-linux-amd64"
chmod +x /usr/local/bin/fadebox
# pin this build's images on the environment, then bring the instance up and wait for it
fadebox env service set api --image-tag "$CI_COMMIT_SHA"
fadebox env service set web --image-tag "$CI_COMMIT_SHA"
I="pr-$CI_MERGE_REQUEST_IID"
# say what the copy is for, wait for it, and take the ingress cookie in the same step
fadebox instance up "$I" --wait --print-token -o json \
--purpose "$CI_MERGE_REQUEST_TITLE" \
--link "MR !$CI_MERGE_REQUEST_IID=$CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID" \
> deploy.json
URL=$(jq -r '.containers.web.urls[0]' deploy.json)
echo "Preview: $URL"
# … run the suite against $URL; where the instance is gated, the test runner sets
# .accessToken.cookie to the matching .accessToken.tokens[].value on that host …
fadebox instance check "$I" e2e --state passed \
--summary "84 passed, 0 failed" --run-url "$CI_JOB_URL"
If the deploy fails, instance up --wait exits 1 with the reason and the pipeline stops there.
Report --state failed from the job's failure path and the instance carries the bad news on its
own card, where whoever opens the merge request will look.
The teardown — fadebox instance stop "$I" --wait then fadebox instance delete "$I" — belongs
on the merge or close event rather than at the end of this job. The
purpose, the check and the verdict are only
worth recording on a preview somebody can still open; a stop three seconds after the suite passes
leaves nothing to review. What keeps that from accumulating copies is the
expiry, not the pipeline.
Note that the image tags land on the environment, so every instance of dev follows them —
right for one version stream per environment. Streams that must not share versions are separate
environments (fadebox env clone dev dev-v2, target with FADEBOX_ENV). Editing an environment
is a maintainer act, so a pipeline that pins versions needs its key issued
with that role — or no write access at all: with a git value source as
the tag, the env service set calls disappear entirely, the deploy reads the version from your
values file, and the default deployer key suffices.
Coming from the 1.4 CLI
Up to 1.4 the CLI was a native Java build for Linux on amd64 only. From 1.5 it is a Go program,
which is what lets it run on six platforms and carry a network tunnel. The name and the download
URL stay: /cli/fadebox-linux-amd64 now hands out the new build, so a pipeline's curl line needs
no change. The notices are now /cli/fadebox-THIRD-PARTY-NOTICES.txt; the 1.4 name,
/cli/fadebox-cli-THIRD-PARTY-NOTICES.txt, still answers with the same file.
It is a drop-in. The commands, flags, tables, error messages and exit codes are those of the
1.4 CLI. It reads and writes the same ~/.config/fadebox/config.yaml and the same FADEBOX_*
variables, so a machine that is logged in stays logged in. Four differences are deliberate:
-o jsonand-o yamlprint the server's response as it is, every field in the server's order andnulls included, where the 1.4 CLI printed only the fields it knew.jqpaths that worked before keep working.- A server URL that is not
http(s)://host[:port]is refused as a usage error (exit2) before anything is sent, where the 1.4 CLI failed on the connection. - Help and usage text are laid out differently.
HTTPS_PROXY,HTTP_PROXYandNO_PROXYare honoured (see Proxies), where the 1.4 CLI ignored them and always went to the server directly. A machine whose proxy variables are set for other tools and that reaches Fadebox directly lists the server inNO_PROXY.
Two refusals are new:
- A redirect is never followed, by any command or by
connect's tunnel. A server that answers with one is refused with The server redirected (HTTP 302) to …; redirects are not followed, naming where it points: following it could replay aDELETEas aGETand send your key on to wherever it points. Set the context's URL to the server's own address. .and..are refused as an instance, a service, a project or an environment — a usage error (exit2) before any request, since either would address a different resource than the one you named.
fadebox connect is new. It reaches an instance's databases and other services from your own
machine, through the instance agent. Each service port gets a stable local port, so psql
connects to localhost:5432:
fadebox connect pr-42
Connecting with fadebox connect has the
details: which ports it opens, how it remembers local ports, --port, and when it uses the
tailnet instead.
Not yet
logsis a snapshot, not a stream.-fwould mean polling and de-duplicating a tail window, which is wrong at the edges; it waits for a streaming endpoint rather than being faked.- No
apply. Pushing a directory of template and environment manifests into an installation is designed but unbuilt; today the CLI drives instances and edits environments — it does not create templates or environments from scratch. - No notarisation or code signing. The macOS builds carry only the ad-hoc signature Apple
silicon requires, and the Windows builds none, so a build saved through a browser may be held
by Gatekeeper or SmartScreen on its first run; one fetched with
curl, as above, carries no quarantine mark and runs as downloaded.