Ingress and instance URLs
Mark a service's HTTP port and every instance of it gets a stable, predictable URL. No per-deploy proxy configuration, no host-port allocation, no collisions between instances.
How the URL is built
{namespace}-{service}.{ingress-domain}
└────────────┬───────┘
the container name shop-staging-pr42-web.envs.example.com
The namespace is {project}-{environment}-{instance}, so the whole hostname is a single DNS
label — that is what lets one wildcard record and one wildcard certificate cover every instance
you will ever deploy. Multi-level subdomains would break both.
The ingress domain is a property of the runtime, which is how traffic
reaches the right host when you run several: envs.example.com for local, eu.envs.example.com
for eu-prod. A runtime with no ingress domain has no instance URLs — publishing host ports still
works everywhere.
Setting it up
- Point a wildcard DNS record at the runtime host:
*.envs.example.com → 203.0.113.10. The record points at the Docker host the runtime runs on, not at the Fadebox server: instance traffic goes straight to the Traefik on that host and never passes through the control plane. With several runtimes, each ingress domain points at its own host. For a local try-out,localtest.meresolves*.localtest.meto127.0.0.1with no DNS setup at all. - Set the ingress domain on the runtime under Settings → Runtimes, along with whether URLs are HTTPS. Whether a URL asks for a sign-in is not decided here — it is each instance's own setting.
- Install the ingress stack from the runtime's Ingress action in the list. Fadebox deploys
Traefik plus a read-only Docker socket proxy onto that runtime, labelled as system containers,
and the dialog shows their live status and whether they still match the saved settings:
up to date, or outdated once a setting, a Fadebox release or the egress proxy changed
since they were created. Update applies the saved settings; Remove tears them down.
Saving ingress settings never touches the running stack — the save ends by offering the
dialog, and the dialog decides on the host's own word, not on what was typed. On a daemon two
Fadebox installations share, the status, the update and the removal reach this
installation's stack only: a neighbour's is neither reported as yours nor replaced by an
update of yours. If a container of another installation already holds one of the stack's
names, the install is refused with
ingress_stack_foreign(409), naming the container and the installation stamped on it, and that container is left alone. If that installation is gone, remove the container on the host (docker rm -f <name>) and install again; if it is still running, install this ingress on another host.
Traefik derives its routes from container labels by watching the daemon's event stream, so a deploy needs nothing pushed to it and there is no proxy config to drift out of sync. Fadebox stamps the labels; it never manages proxy state.
Do not run the managed stack and a hand-installed proxy on the same ports — by default the stack binds 80 and 443. On a host where those are taken, move the ingress to a free port — see custom ports below.
How the stack reaches Fadebox
Traefik in every managed stack also asks Fadebox for routes, every three seconds: those of the elastic runtimes that use this runtime as their front. A stack that fronts nothing gets an empty answer, and routes by labels as described above. The request carries a secret generated for that stack. So the host a stack runs on has to be able to reach Fadebox, and Fadebox tells Traefik where:
FADEBOX_PUBLIC_URL, when it is set. It must not be a loopback address —localhost,127.0.0.1,::1: inside Traefik's container that is the container itself, so installing or updating a stack is refused (ingress_gate_unavailable,reason: public_url_loopback). For a try-out on a laptop, leave the setting unset.- Otherwise, an address Fadebox works out from the runtime's daemon. On a local daemon
(
unix://) it ishost.docker.internalon Fadebox's own port, 8080, which reaches Fadebox through the port the shipped compose file publishes. On a remote daemon (tcp://) it is the address Fadebox's own connection to that daemon leaves from, on the same port — right for a Fadebox installed directly on a host that shares a network with the daemon, a private LAN address included. If that daemon does not answer, the install is refused (reason: control_plane_unreachable) and the running stack is left as it is.
Where Fadebox can tell that the worked-out address cannot work, it refuses to install or update
the stack (ingress_gate_unavailable, 409) rather than hand Traefik an address nothing answers
on, and the running stack is left as it is:
- Fadebox runs in a container and the runtime's daemon is remote —
reason: control_plane_address_unusable. The address the connection leaves from is the container's own, which the runtime's host never sees. This is the shippeddocker-compose.ymlwith a remote static runtime: setFADEBOX_PUBLIC_URLto an address the runtime's host can reach Fadebox on. - The address found is loopback or link-local — the same reason, and the same fix.
- Fadebox listens on a loopback address only —
quarkus.http.hostset tolocalhost,127.0.0.1or::1, as on a bare-metal install behind a local nginx —reason: control_plane_bind_loopback, for local and remote daemons alike. A container reaches its host through the host's gateway address, never through its loopback, so nothing would answer. SetFADEBOX_PUBLIC_URL, or have Fadebox listen on0.0.0.0; every shipped image does.
What Fadebox cannot tell in advance — NAT or a port-forward between the two, Fadebox published on
a port other than 8080 (FADEBOX_PORT in the shipped compose file), rootless Docker's host
gateway — it sees afterwards, because the stack stops polling (see
below). In all of these cases set FADEBOX_PUBLIC_URL to an address the
runtime's host can reach; when it is set it always wins, and nothing is worked out or probed.
Until then a stack that fronts nothing still routes its own instances; only an elastic runtime
behind it would get no routes. On a proxied network the worked-out address is added to Traefik's
NO_PROXY.
The runtime's Ingress dialog says ahead of time when an install or update would be refused, with the same sentence the refusal carries, so a stack that reads outdated also tells you what to fix before Update can go through.
After upgrading to 1.4, every stack installed by an earlier release shows outdated once, because
it has no route polling yet. Update applies it. On an installation where Fadebox runs in a
container and a static runtime's daemon is remote, or where Fadebox listens on a loopback address
only, Update is refused until FADEBOX_PUBLIC_URL is set — set it first, then update.
Is the stack polling?
Whether the stack is actually reaching Fadebox is something Fadebox observes rather than works out: it records every authenticated routes poll from a stack's Traefik, and the Ingress dialog shows the result under the stack's status:
- OK — the last poll arrived within the last minute; the dialog reads Polling fadebox for routes and says when.
- Waiting — no poll yet since the stack was applied or Fadebox started, and that was under a minute ago. The first poll is due within seconds of Traefik starting.
- Stale — no poll for over a minute. The stack cannot reach the address it was given, so no
elastic instance behind it is reachable, even though the dialog may read up to date. The
message names the likely causes —
FADEBOX_PORTset to something other than 8080, Fadebox listening on a loopback address only, NAT between the runtime's host and Fadebox, rootless Docker's host gateway — and the fix: setFADEBOX_PUBLIC_URLto an address the runtime's host can reach Fadebox on, then Update.
The record is kept in memory on each Fadebox node, so a restart starts it afresh; a working stack
polls again within seconds. Over the API it is the poll block of
GET /api/runtimes/{name}/ingress — lastPollAt, verdict (OK, WAITING or STALE) and a
message — beside applyRefused, which carries the reason and message an install or update
would be refused with, and gateUnavailable, which says why a gated instance cannot deploy here
(see Requiring a sign-in). The CLI has no command for the stack's status.
SELinux hosts
Since 1.3.0 the socket proxy runs with the daemon security option label=disable. On a host where
SELinux is enforcing and the daemon runs with its SELinux integration on (Fedora and RHEL by
default), a confined container cannot open the bind-mounted daemon socket, and without the option
the stack installs cleanly and routes nothing: the proxy answers 503 to Traefik, no router is ever
created, and every instance URL is Traefik's 404 page not found while both containers are Up
and the dialog says up to date. The option is a no-op on a host without SELinux, so it is always
set; Traefik never touches the socket and stays confined. A stack installed by an earlier release
shows outdated after upgrading — Update recreates the proxy with the option. The manual
deploy/ingress bundle carries the same security_opt on its socket-proxy service.
Hosts that cannot reach Docker Hub
The stack is two images Fadebox pins per release and pulls from Docker Hub — in this release
traefik:v3.7 and tecnativa/docker-socket-proxy:v0.5.0. Templates name their own images and
registries; these two are the only ones Fadebox chooses for you, so on an air-gapped runtime host
they are the only ones you have to provide. Either of these makes the install go through:
- Load them onto the host. Pull both on a connected machine,
docker savethem, anddocker loadthem on the runtime host. The stack pulls only what the daemon does not already hold, so an image present on the host is never fetched. - Mirror them into a registry the host can reach, then set the registry prefix on
Administration → System (global
admin):registry.corp:5000/mirror, no scheme. It is prepended verbatim, so the stack pullsregistry.corp:5000/mirror/traefik:v3.7— the version pins stay Fadebox's and follow each release, only the source moves. If that registry needs a login, add it to the runtime under Settings → Runtimes like any other registry credential; the credential is matched on the registry host of the resolved reference, and the stack's images are what a runtime-level credential exists for. The prefix is installation-wide and read at the next Install or Reinstall / Upgrade of the stack on any runtime — no restart. The page says whether the value in force was saved there, came fromFADEBOX_IMAGE_REGISTRYor is the default, and Reset to default withdraws a saved one; every change is audited.
An install that fails on the pull says so, naming the two references it tried and both remedies,
rather than surfacing the daemon's raw error. A registry-mirrors entry in the daemon's own
configuration also works with no Fadebox setting at all, if you can edit that on every runtime
host.
TLS
For HTTPS, the runtime's HTTPS certificate setting picks how the managed stack certifies it:
- Let's Encrypt (the default): give the runtime ACME settings — an email address, a DNS
provider and its credentials, as
VAR=valuelines. Traefik then issues a wildcard certificate over DNS-01, which is the only challenge type that can cover*.envs.example.com. - Traefik default certificate: Traefik serves the self-signed certificate it mints at startup.
No ACME settings, no public DNS to solve a challenge against, no certificate authority to ask —
which suits an internal or air-gapped runtime that wants deployed apps served as
https://so browsers stop treating them as insecure origins, and nothing more: the connection is encrypted but unauthenticated, so every browser warns and other clients need an explicit trust exception. Switching modes keeps the stored ACME settings; switching back is a mode change and an update of the stack.
The credentials are write-only — accepted by the API, never returned. They end up as environment variables on the Traefik container, which is visible to anyone who can administer that daemon; that is the same trust level as the daemon itself.
Certificates live on a volume that survives upgrade and removal of the stack, so re-installing does not re-issue them. If you already hold certificates and would rather mount them, stay on a hand-installed Traefik.
On a network that only reaches the internet through a proxy, the managed stack inherits the
server's egress proxy: Traefik is started with
HTTP_PROXY, HTTPS_PROXY and NO_PROXY matching QUARKUS_PROXY_HOST, with its backends and the
socket proxy kept direct. There is nothing to set on the runtime; re-apply the stack after changing
the proxy. If any instance on the runtime requires a sign-in and Fadebox itself is only reachable
directly, list its host in QUARKUS_PROXY_NON_PROXY_HOSTS so the sign-in check does not go through
the proxy.
Provider credentials
Read lego, not Traefik. Traefik keeps no provider list of
its own — it delegates the DNS-01 challenge to the lego library and points at that project's
documentation — so lego's DNS providers page is the source of truth for both fields, and each
provider has a page of its own: cloudflare,
gcloud, and so on.
The provider name is the value in that page's CLI flag name column, exactly as written:
cloudflare, gcloud, route53. The credentials are the variables from the provider's own
page, one VAR=value per line — for token-based providers, a single line:
CF_DNS_API_TOKEN=<token with DNS edit rights on the zone>
Ignore everything lego says about files. It lets any variable take a _FILE suffix and read its
value from a path, and some providers document a file variable first, but the credentials reach
Traefik only as environment variables — nothing mounts a key file into the managed container, so
a path there points at nothing. A provider needs an inline variant to be usable with the managed
stack. Google Cloud DNS (gcloud) is the common case where that variant takes some finding —
lego's page leads with application-default credentials and GCE_SERVICE_ACCOUNT_FILE, and neither
works here:
-
The ingress domain must be a managed zone in Cloud DNS, holding the runtime's wildcard record.
-
Create a service account with the DNS Administrator role and download a JSON key:
gcloud iam service-accounts create fadebox-acmegcloud projects add-iam-policy-binding PROJECT_ID \--member serviceAccount:fadebox-acme@PROJECT_ID.iam.gserviceaccount.com \--role roles/dns.admingcloud iam service-accounts keys create key.json \--iam-account fadebox-acme@PROJECT_ID.iam.gserviceaccount.com -
Set the runtime's credentials to the project id plus the key minified to one line (
jq -c . key.json):GCE_PROJECT=PROJECT_IDGCE_SERVICE_ACCOUNT={"type":"service_account","project_id":"PROJECT_ID",…}GCE_SERVICE_ACCOUNTtakes the key JSON itself;GCE_SERVICE_ACCOUNT_FILEis the path variant and cannot work here. If issuance times out while the challenge record propagates, addGCE_PROPAGATION_TIMEOUT=300as another line — lego's default is 180 seconds.
Reading the ingress logs
The managed Traefik logs at INFO, which is what makes certificate and routing activity
visible — Traefik's own default, ERROR, is silent until something has already failed, so a
certificate that was never requested and one issued fine look identical. Read it on the runtime
host:
docker logs fadebox-ingress-traefik # the socket proxy is fadebox-ingress-socket-proxy
Fadebox does not surface these logs in the UI; the log tail and export cover an instance's own containers, not the shared proxy. These are Traefik's application logs — per-request access logging is not enabled at either level. They are ordinary container logs, so the daemon-wide cap in Container logs bounds them like everything else on the host; Fadebox sets no limit of its own.
When INFO is not enough, Settings → Runtimes has Debug ingress logging on the runtime's Ingress card. It applies on the next Reinstall / Upgrade of the stack, not on save — and that re-apply briefly takes ingress down for every instance on that host, so it is not a free operation on a busy one. Issued certificates survive it.
Turn it off again once you have your answer. Beyond the volume, the Traefik container holds the runtime's DNS-provider credentials as environment variables for ACME, and verbose logging around certificate issuance is not where you want to be reading by default.
If you run Traefik yourself instead of the managed stack, none of this applies — it keeps its own
default level until you add --log.level to its command.
Custom ports and reverse proxies
By default the ingress listens on the scheme's standard port — 80, or 443 with HTTPS. Two per-runtime settings change that, both under Settings → Runtimes:
Ingress port moves the proxy off the standard port for hosts where it is taken or unbindable.
The managed stack binds and publishes that port instead, and the port appears in every generated
instance URL and in {{instance.host}} — shop-staging-pr42-web.envs.example.com:8443. With
HTTPS on a custom port, the usual :80 → https redirect listener is skipped: the standard
ports are not the stack's to bind on such a host. If you run the ingress bundle by hand instead
of installing the managed stack, set INGRESS_PORT in its .env and the matching Ingress
Port on the runtime — they must agree, or generated links point at a port nothing listens on.
Public port covers a reverse proxy in front of the ingress, where the two ports legitimately
differ: Traefik binds, say, 8443 while visitors reach the proxy on 443. Without it, every
generated link would carry the internal port. Set the public port and that is what URLs and
{{instance.host}} advertise instead of the bind port; setting it to the scheme default (80/443)
makes links port-free. It changes nothing about what the managed stack binds.
A gated instance's sign-in returns only to a port the ingress answers on. After a visitor signs in, Fadebox sends them back to the instance on the runtime's scheme and on one of two ports: the public port, or the port Traefik binds. A reverse proxy that serves the ingress on any other port while Public port is not set therefore has its sign-ins refused, with a message naming the address the redirect has to be on — set the public port to the proxy's. For an instance on an elastic runtime, these are the front runtime's settings.
The public port assumes the fronting proxy passes TLS through to the ingress. Terminating TLS at the proxy and speaking plain HTTP behind it is not supported — the URL scheme is coupled to the runtime's own HTTPS/ACME setup.
Single-origin applications
One hostname per service is wrong for an application served as one origin — an SPA at / whose
API lives under /api/* and which calls it with relative URLs. Those services have to share a
hostname.
Give a port a path and it moves onto the instance host, {namespace}.{ingress-domain}, which
every path-routed service of that instance shares:
services:
web:
image: shop/web:1
labels:
fadebox.ingress.port: "80"
fadebox.ingress.path: "/" # claims the instance host
api:
image: shop/api:1
labels:
fadebox.ingress.port: "8080"
fadebox.ingress.path: "/api"
fadebox.ingress.strip: "true" # the app sees /orders, not /api/orders
- Order does not matter. The longest prefix wins, so
/api/ordersbeats/api, which beats/. Nothing has to be sorted or configured. /is a valid path and does not stop siblings from claiming deeper prefixes. Stripping/is rejected — there would be no rooted path left.- Mixing is fine. A service without a path keeps its own
{namespace}-{service}hostname, so an admin UI can live on a separate origin while the app is single-origin. - Collisions fail the deploy, not the request: if two services in an environment would produce the same route, the deploy is rejected naming both. A proxy would otherwise pick one silently.
A multi-port service assigns paths positionally — one path per port, in the same order as
fadebox.ingress.port.
Requiring a sign-in
An instance's Access says who may open its URLs. It is one of three levels:
- Open — anyone who can reach them, with no sign-in.
- Members — the project's members, meaning anyone who holds a role in it (
vieweror above, granted through a group like every project role), and administrators. - Creator — the instance's creator while they are a member of the project, the project's editors, and administrators: the people who may operate a personal instance. A creator who has left the project is refused like any other former member, and an instance whose creator's account is gone lets in its editors.
A visitor who is not signed in is sent to the Fadebox sign-in page and bounced back. One who is
signed in and not let in lands on a Fadebox page that names the instance's project and says to ask
one of its maintainers for access. Both closed levels ask about the instance's project, so no
level lets anyone in merely for having a Fadebox account. To open an instance to a whole
department, grant the department's group viewer on the project — see
Who gets which role. A global admin gets through every
gate; when the level would have refused them — no role in the project, or under Creator neither
its creator nor an editor — the admission is recorded in the audit log as
ingress.admin_admitted, with the host and the project.
The level is the instance's own. Two instances on one runtime can differ — the internal tool
closed, the customer demo beside it open — and moving an instance to another runtime changes
nothing about who can reach it. An environment sets the level its new instances start at
(New instances, in the environment's settings), and whoever creates an instance can pick
another for the one they are creating — Access on the create form, --gate in the
CLI. Afterwards it changes under Access in the instance card's
More menu, or with fadebox instance gate / ungate.
Raising the level is for whoever may operate the instance; lowering it takes editor. A step
up — Open to Members, Members to Creator — is any deployer's to do, and on a personal instance
its creator's and the project's editors'. Any step down widens who sees the project's work, so it
takes editor or above, even from the instance's creator; so does creating an instance at a
level below its environment's default. Both are refused below that with
instance_gate_restricted (403).
:::note Upgrading from 1.3 or earlier
Until 1.4 this was one switch per runtime. The upgrade carries it over: every instance on a
runtime that required a sign-in is set to Members. So are the new instances of every
environment whose project could deploy onto such a runtime — a shared one, or one granted to
the project — whether or not the environment has an instance there today. An environment whose
long-lived instance runs on an open runtime while its per-pull-request instances land on a gated
one is therefore gated too. This errs closed on purpose: an environment that should start its
instances open takes one change, New instances in its settings, and one that opened itself
would go unnoticed. The runtime switch is gone, and an API call that still sends
ingressAuth: true for a runtime is refused with a message saying so, rather than quietly
leaving the runtime open.
:::
Opening or closing the gate takes effect on the instance's next deploy. Whether visitors are
asked at all is a label on the instance's routes, so the containers running now keep the one they
were created with. Until then the card names what is in force first and what the next deploy
makes it after: open, members on next deploy means the URLs still answer anyone, and
members, open on next deploy means they still ask for a sign-in and let in the project's
members. fadebox instance list reads open → members. A plain members or creator means it
is in force.
A move between Members and Creator needs no deploy. It applies to the next sign-in at once. A visitor let in before it keeps their session until its cookie expires, which is an hour at most.
Two things have to be true of the installation for a gate to work, and Fadebox refuses rather than leaving you to find out from a broken URL:
- Fadebox has to know the address users open it on — see
FADEBOX_PUBLIC_URLbelow. Without it, closing an open gate is refused. - The runtime's managed ingress stack has to carry the sign-in check. Every stack installed or
updated by 1.4 on an installation with
FADEBOX_PUBLIC_URLset does. One installed by an earlier version, on a runtime that did not require a sign-in then, does not: it shows as out of date in the runtime's Ingress dialog, and a gated deploy onto it is refused until an admin presses Update there. The manualdeploy/ingressbundle has always carried it.
The runtime's Ingress dialog says which of the two stands in the way, because the fixes
differ. Without FADEBOX_PUBLIC_URL no stack carries the check, however new, and the dialog says
that instances on this runtime cannot require a sign-in until the setting is made and the stack is
then updated — Update on its own changes nothing. A stack that merely predates the check reads
that Update adds it. Over the API it is gateUnavailable on
GET /api/runtimes/{name}/ingress: PUBLIC_URL or STACK_OUTDATED — the reason a gated
deploy is refused with (ingress_gate_unavailable, 409), in upper case — or null when nothing
stands in the way. A host with no stack at all reads null; it is not the gate that is missing
there.
Gated instances and published ports
A browser sends a host's cookies to every port of that host, so a container that publishes a port
on the host a gated instance's URLs point at would receive its visitors' sign-in cookies. On a
static runtime Fadebox therefore keeps the two apart, in either order, and refuses with
ingress_host_port_conflict (409):
- a gated instance whose own services publish a port — when the gate is closed, when the instance is created closed, and at every deploy;
- a deploy that would publish a port on a runtime whose host already serves a gated instance's URLs;
- a gated deploy whose URLs would be served by a runtime on which another instance publishes a port.
A deploy is held to these rules as it is recorded, one deploy at a time per host, so of two deploys that would break them, the later is refused however close together they start. For the second rule an instance counts as gated from the moment a deploy with its gate closed starts, not only once that deploy has put its URLs behind the sign-in. A gate closed while a deploy is already under way counts too, and refuses a neighbour's port until that deploy ends, although the gate itself takes effect only at the next deploy.
Any ports: entry counts: "5432" as much as "15432:5432", since Docker then publishes the
container port on a host port of its own choosing, which answers all the same. An
ingress port needs no ports: entry, and without one it
does not count. An instance with no URLs — no ingress port, or a runtime without an ingress
domain — has no sign-in to leak and is not refused for its own ports. The refusal names the
runtime, and the service and port when they are your project's; another project's instance,
service and port are never named. The way through is to drop the port — a sibling reaches a
service over the instance network without one — to put one of the two on another runtime, or to
open the gate. An instance on an elastic runtime has its URLs served by
the front, so the front is the host that counts for its gate; its machine's own ports are never
refused, since no hostname points at a machine. If its gate was closed while the machine was
starting, the front's ports are checked once the machine is ready, before any container starts;
refused there, the instance ends in ERROR with the refusal's message, and its machine is
stopped.
How the sign-in works
The mechanism is worth understanding, because it is designed around one constraint: the Fadebox session cookie must never reach instance hosts. A cookie widened to the parent domain would be sent to every deployed — and therefore untrusted — container, handing any preview app the operator's session. Instead:
- Traefik asks Fadebox whether the request is allowed. Without a valid cookie for this instance, the visitor is redirected to Fadebox's own origin.
- There, where the real session is valid, Fadebox checks that the target is a hostname a gated instance is actually answering on, on the scheme and port the ingress serves it on (so it cannot be used as an open redirect, nor to hand the token to another port of the host), and that the signed-in person passes the instance's level. It then mints a 60-second token bound to that exact hostname and that instance.
- The token is exchanged for a cookie scoped to the instance host only, valid an hour, or until the instance expires if that comes sooner. The cookie names its instance, so it is useless against any other — including a new instance that later takes over the same hostname. When it runs out the browser is sent round the same loop, which the Fadebox session answers without asking anything.
The deployed application sees only that per-host cookie — which is valid for its own hostname
anyway — and an X-Fadebox-User header naming the signed-in user.
The level is checked in step 2, when the cookie is handed out, not on every request after it. That is why a session admitted before a move to Creator runs on until its cookie expires, and why the cookie lasts no longer than an hour: someone removed from the project keeps a cookie they already hold for at most that long.
This flow needs Fadebox's own browser-reachable address, so it needs
FADEBOX_PUBLIC_URL: the redirect in step 1 goes there
and nowhere else, and a stack defines the sign-in check only when the setting is set. Traefik
calls the same address for every request to a gated instance, so the runtime's host has to reach
it too. The redirect back in step 3 goes only to the instance's own origin — see
Custom ports and reverse proxies for what that means behind
a proxy.
Letting a pipeline through
The flow above speaks only to browsers, and a redirect to a sign-in page is no use to a test runner — so a pipeline could deploy a gated instance and then not be able to open it. Whoever may operate the instance can mint the cookie directly instead:
fadebox instance token pr-1842
Cookie: fadebox_ingress
Expires: 2026-09-15T08:12:44Z
HOST VALUE
shop-dev-pr1842-web.envs.example.com eyJhbGciOi…
Over plain HTTP that is POST .../instances/{slug}/access-token, which answers:
{
"cookie": "fadebox_ingress",
"expiresAt": "2026-09-15T08:12:44Z",
"tokens": [{ "host": "shop-dev-pr1842-web.envs.example.com", "value": "eyJhbGciOi…" }]
}
One value per hostname the instance holds. Set it as that cookie on that host in whatever
drives the browser — context.addCookies in Playwright, cy.setCookie in Cypress, --cookie
for curl — and the gate lets the request through. It is the same per-host cookie a browser ends
up holding after step 3, valid an hour or until the instance expires, bound to the instance and
useless against any other host, and it names the pipeline's own service account: the deployed
application sees it in X-Fadebox-User exactly as it would a person.
It goes only to someone the instance's level would let in through the browser. A deployer is a
member of the project, which is all Members asks; under Creator only the instance's
creator, the project's editors and an admin get one, and anyone else is refused with
instance_access_denied (403) before anything is minted. A service account that creates its own
per-pull-request instances is their creator, so a Creator gate does not stop its own pipeline.
Two things make it safe to put in a pipeline unconditionally. An instance deployed open answers
with an empty tokens list rather than refusing, so one job step works against open and gated
instances alike and nothing has to know in advance which it is talking to. And nothing is
stored — every call mints fresh values — so there is no token sitting in Fadebox to leak and
nothing to revoke. The audit log records which hostnames a call covered
(instance.access_token_issued) and never the values themselves.
fadebox instance up <slug> --print-token rolls the deploy and the mint into one step: it implies
--wait and adds an accessToken key to the status document it prints, so a job reads the URL
and the cookie out of the same JSON. There is deliberately no MCP tool for this — see
what has no tool at all.
What is not covered
- Non-HTTP traffic. Databases and other TCP services keep using published host ports — routing raw TCP through Traefik requires TLS with SNI, which does not apply here. On a static runtime such a port cannot sit beside a gated instance — see Gated instances and published ports.
- Instance-to-instance isolation on the edge network. Every ingress-exposed container shares one bridge network with Traefik, so those containers can reach each other's exposed ports directly. That is the same trust domain as the shared host itself.