Skip to main content
Version: 1.3

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 and whether they require a Fadebox sign-in.
  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.

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.

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 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.

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​

Turning on Require fadebox sign-in for a runtime gates every instance URL on it behind a Fadebox session. Anonymous visitors are sent to the sign-in page and bounced back.

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 per-host cookie, the visitor is redirected to Fadebox's own origin.
  2. There, where the real session is valid, Fadebox checks the target host against the ingress domains of enabled runtimes (so it cannot be used as an open redirect) and mints a 60-second token bound to that exact hostname.
  3. The token is exchanged for a cookie scoped to the instance host only, valid 12 hours. A cookie issued for one instance is useless against any other.

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.

Authorization is authentication-only: any signed-in Fadebox user may open any gated instance URL. Per-instance rules are not implemented.

This flow needs to know Fadebox's own browser-reachable address. Set FADEBOX_PUBLIC_URL if the URL Traefik reaches Fadebox on is not the same one a browser would use.

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 on a gated runtime a pipeline could deploy an instance and then not be able to open it. A project deployer 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 12 hours 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.

Two things make it safe to put in a pipeline unconditionally. A runtime without the gate answers with an empty tokens list rather than refusing, so one job step works against both kinds of runtime 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.
  • 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.