Skip to main content
Version: Next

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​

  1. 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.me resolves *.localtest.me to 127.0.0.1 with no DNS setup at all.
  2. 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.
  3. 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.

caution

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 is host.docker.internal on 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 shipped docker-compose.yml with a remote static runtime: set FADEBOX_PUBLIC_URL to 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.host set to localhost, 127.0.0.1 or ::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. Set FADEBOX_PUBLIC_URL, or have Fadebox listen on 0.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_PORT set 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: set FADEBOX_PUBLIC_URL to 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 save them, and docker load them 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 pulls registry.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 from FADEBOX_IMAGE_REGISTRY or 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=value lines. 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:

  1. The ingress domain must be a managed zone in Cloud DNS, holding the runtime's wildcard record.

  2. Create a service account with the DNS Administrator role and download a JSON key:

    gcloud iam service-accounts create fadebox-acme
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member serviceAccount:fadebox-acme@PROJECT_ID.iam.gserviceaccount.com \
    --role roles/dns.admin
    gcloud iam service-accounts keys create key.json \
    --iam-account fadebox-acme@PROJECT_ID.iam.gserviceaccount.com
  3. Set the runtime's credentials to the project id plus the key minified to one line (jq -c . key.json):

    GCE_PROJECT=PROJECT_ID
    GCE_SERVICE_ACCOUNT={"type":"service_account","project_id":"PROJECT_ID",…}

    GCE_SERVICE_ACCOUNT takes the key JSON itself; GCE_SERVICE_ACCOUNT_FILE is the path variant and cannot work here. If issuance times out while the challenge record propagates, add GCE_PROPAGATION_TIMEOUT=300 as 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.

note

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/orders beats /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 (viewer or 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_URL below. 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_URL set 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 manual deploy/ingress bundle 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; nor does an expose: entry, which publishes nothing. 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:

  1. Traefik asks Fadebox whether the request is allowed. Without a valid cookie for this instance, the visitor is redirected to Fadebox's own origin.
  2. 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.
  3. 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, or native access — 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.