traefiklinker-service is the platform’s programmatic HTTP edge — the daemon that publishes and manages reverse-proxy routes from your playbooks, so putting a service on a public URL becomes just another automated step. Binions ships with a Traefik edge that fronts the platform; traefiklinker is the daemon that drives it. When a playbook asks to expose something at a public address, traefiklinker writes the matching route into Traefik’s configuration, and the edge starts serving it — routing the hostname to your service, terminating TLS, obtaining the certificate, and enforcing whatever guardrails the route declares: authentication, IP allowlists, rate limits, custom headers, path rules, even weighted traffic splitting across several backends. You never hand-edit a proxy config; you describe the route, and the daemon makes it live.
Good to know. traefiklinker does not carry your web traffic — the Traefik edge does that. traefiklinker is the control plane: it turns a playbook step into a route definition the edge picks up. The clean split means routes are created, listed, and removed the same way you do everything else on Binions — with a short playbook verb — while the battle-tested edge handles the actual proxying and certificates. New to the platform? Start with Core concepts.
Binions is event-driven: its daemons talk over a fast internal event bus, and everything that happens is described as an event riding in a standard envelope. traefiklinker turns “expose this at a URL” into events the rest of the platform understands, and translates them into live proxy routes.
A daemon such as showman-service serves its pages on a local address that only the host can reach. That is deliberate — nothing is exposed by accident. To make a page or an API reachable from your network or the public internet, you register a route: a public hostname, the internal backend — or several, with traffic weights — that should answer it, the protocol to speak, and optionally the path it matches and the middlewares that guard it. traefiklinker writes that route into the edge’s dynamic configuration, and the edge does the rest.
http:// on the loopback address while the public side is properly encrypted. You name the resolver on the route; the edge obtains and renews the certificate.http:// URL. The route is the only thing that becomes public; the backend stays where it was.That is the big idea: going from “a service running on the loopback address” to “a service answering at https://app.example.com — for the right callers, at the right rate” is one playbook step. The hostname, the backend, the protocol, the TLS certificate, and the guardrails in front of it are all described in a few lines, and the edge picks them up automatically.
Two layers, kept separate. The platform’s own built-in entry points — the main HTTPS edge and the bundled operator tools (Traefik dashboard :8444, MinIO console :8445, Showman hub :8446) — are fixed configuration rendered when the host is bootstrapped; they are not managed through playbooks. traefiklinker is for the routes you publish on top of that edge. See Network requirements for the fixed entry points, and Bind addresses for how bootstrap sets where the edge listens.
| What it is | The programmatic HTTP edge — publishes and manages reverse-proxy routes from playbooks |
| Playbook prefix | traefik. — e.g. traefik.register_route |
| What it controls | The platform’s Traefik edge (the actual proxy) — see Platform infrastructure |
| How it works | Writes Traefik’s dynamic file-provider config; the edge auto-reloads and serves the route |
| TLS | Terminated at the edge via a certificate resolver — backends stay plain HTTP on loopback |
| A route can express | Hostname + optional path-prefix matching · seven protocols · middlewares (basic auth, forward auth, IP allowlist, rate limiting, custom headers) · a single backend or a weighted set |
| Operations | Register a route, unregister a route, list routes, reload |
| Service | binions-traefiklinker.service with a dedicated redis-binions-traefiklinker.service |
In a playbook you address the daemon with the lowercase verb form run: traefik.<operation>, which the platform turns into the daemon’s internal Action.Traefik.<Verb>. There are four operations — one to publish a route, one to retire it, one to list what is published, and a reload acknowledgement:
| Operation | What it does | Arguments |
|---|---|---|
traefik.register_route | Publish a route: map a public hostname (and, optionally, a path) to one or more internal backends — with any middlewares it should carry — and bring it live at the edge. Typically a provisioning step, run once when a service should become reachable. | a single route object carrying name, host, backend (or weighted backends), and the optional protocol, entry_points, path_prefix / strip_prefix, middlewares, and TLS settings (see fields below) |
traefik.unregister_route | Retire a route by name. The edge stops serving that hostname; the backend itself is untouched. | name |
traefik.list_routes | List the routes the platform manages. It merges the routes written to disk with a best-effort live view from the edge’s admin API, so you see both what should be there and what the edge currently reports. | — none — |
traefik.reload | An acknowledgement, not an action: the edge already watches its configuration and reloads on its own, so this is a no-op you can call for symmetry. You do not need it after registering a route. | — none — |
A route is described by a set of fields, passed as a single route: object under with: on traefik.register_route:
| Field | Meaning |
|---|---|
name | A short identifier for the route — lowercase letters, digits, and hyphens ([a-z0-9-]+), up to 64 characters. You use it later to unregister the route. |
host | The public hostname the edge should answer on — for example app.example.com. |
backend | The internal service the route forwards to. Must be a full http:// or https:// URL — usually a loopback address and port. |
backends | The weighted alternative to a single backend: a list of entries, each with a url (a full http:// or https:// URL) and a weight (defaults to 1). The edge splits traffic across them in proportion to the weights — see below. |
path_prefix | An optional path the route matches. It is ANDed onto the host rule, so the route answers only for requests under that prefix — other paths on the same hostname stay unrouted (or go to other routes). |
strip_prefix | Remove the matched path_prefix before forwarding, so the backend sees the bare path. Applied after the route’s middleware chain has run. |
protocol | How the edge should talk to the backend — one of the protocols listed below. Defaults to https. |
entry_points | Which edge entry points the route is attached to. Defaults to the plain-HTTP (web) and HTTPS (websecure) entry points. |
middlewares | An ordered list of guardrails the edge applies to every request on this route — authentication, allowlists, rate limits, headers. See Middlewares below. |
tls.cert_resolver | The certificate resolver the edge uses to obtain and renew the route’s TLS certificate. |
Mind the prefix and the shape. The verb prefix is
traefik.(the domain isTraefik), so the operations map toAction.Traefik.RegisterRoute,Action.Traefik.UnregisterRoute,Action.Traefik.ListRoutes, andAction.Traefik.Reload. When registering, the whole description nests inside oneroute:object underwith:—name,host,backend,protocol,middlewares, and the rest sit inside it, as in the examples below. Onlytraefik.unregister_routetakes itsnamedirectly underwith:.
A provisioning playbook that runs once at first boot and publishes a page served by showman-service at a public hostname over HTTPS. showman keeps serving on its loopback address; the route is what makes it reachable, and the edge obtains the certificate via the named resolver:
name: provision-status-route
trigger:
event: Fact.System.Boot
filter: { component.eq: playbook-service }
steps:
- run: traefik.register_route
with:
route:
name: status-page
host: status.example.com
backend: http://127.0.0.1:9099
protocol: https
tls:
cert_resolver: letsencrypt
Moments after this step runs, https://status.example.com is live: the edge routes the hostname to showman’s local server, terminates TLS with a certificate from the letsencrypt resolver, and forwards each request inward. Because the route is written to the edge’s dynamic configuration, it is picked up automatically — no reload step needed — and it survives a restart. To retire the page later, a single traefik.unregister_route with name: status-page removes it.
Real-time pages need their connection to stay open. Setting protocol: wss tells the edge this is an encrypted WebSocket route, and it applies long idle timeouts automatically so a quiet-but-open connection is not dropped. This is the playbook step grammar at work — match an event → run a step → the route goes live:
name: provision-live-dashboard-route
trigger:
event: Fact.System.Boot
filter: { component.eq: playbook-service }
steps:
- run: traefik.register_route
with:
route:
name: live-dashboard
host: live.example.com
backend: http://127.0.0.1:9120
protocol: wss
tls:
cert_resolver: letsencrypt
The public side is wss:// (WebSocket over TLS); the backend stays plain http:// on the loopback address, since TLS is terminated at the edge. The same step shape exposes a plain (unencrypted) WebSocket by setting protocol: ws instead. Playbook steps can be combined freely — you can use run:, parallel:, loop:, and wait_for: steps in the same playbook; see Playbook anatomy for the full step grammar.
A route can carry a chain of middlewares: checks and transformations the edge applies to every request before it reaches your backend. They are declared right on the route, as a list under middlewares:, so the guardrails travel with it — register the route and the protection is live; unregister it and everything is removed together.
The list is closed and declarative. Each entry is a flat object whose type field names one of the five kinds below, followed by that kind’s own fields. There is no free-form pass-through to the proxy — a typo’d type, a misspelled field, or a field borrowed from another kind fails loudly when the route is decoded, instead of being silently ignored.
| Type | What it does | Fields |
|---|---|---|
basic_auth | HTTP Basic authentication at the edge — callers must present valid credentials before the request is forwarded. | users — a list of htpasswd-format entries (user:$apr1$…). Reference them as ${secret.*} values in your provisioning playbook so credentials live in the secrets store, not in the playbook file — see Secrets & credentials. |
forward_auth | Delegates the allow/deny decision to an authentication service of your own — the edge consults it for every request. | address — the auth service to consult; optional auth_response_headers — headers copied from the auth service’s reply onto the forwarded request; optional trust_forward_header. |
ip_allowlist | Admits only listed clients; everyone else is turned away at the edge. | source_ranges — bare IP addresses or CIDR ranges. |
rate_limit | Caps the request rate so a chatty client cannot drown the backend. | average — the sustained rate to allow per period; optional burst — short-term allowance above it (defaults to average); optional period_secs — the period the average is measured over (defaults to 1, i.e. requests per second). |
headers | Sets custom headers on the way in and out. | request — a map of headers added to forwarded requests; response — a map of headers added to responses. |
Middlewares run in the order you declare them, and your chain always runs first — before the protocol middlewares the daemon attaches on its own (the SSE header set, the gRPC-Web converter) and before any strip_prefix. So an IP allowlist or a rate limit is enforced before anything else touches the request.
Everything is validated when the route is registered: a malformed CIDR range, an invalid users entry, or a nonsensical rate-limit average is rejected with a failure fact explaining why — a route never goes live half-guarded. Edge middlewares also compose with in-app protections: for example, showman-service can additionally require an HMAC signature on its inbound webhook routes — the edge decides who gets through, the application verifies what arrives.
name: edge-route-ip-allowlist
description: Provisioning — publish the hooks entry, office IPs only, 10 req/s.
trigger:
event: Fact.System.Boot
steps:
- id: route
run: traefik.register_route
with:
route:
name: hooks-in
host: "hooks.example.com"
backend: "http://127.0.0.1:9200"
protocol: https
middlewares:
- type: ip_allowlist
source_ranges: ["203.0.113.0/24", "10.0.0.0/8"]
- type: rate_limit
average: 10
burst: 20
Requests from outside the two listed ranges never reach 127.0.0.1:9200; within them, sustained traffic is capped at 10 requests per second, with short bursts up to 20 allowed. Both guardrails live and die with the route.
Two more parts of the route shape where it matches and where it sends traffic.
Path routing. Set path_prefix and the route matches only requests under that path — the prefix is ANDed onto the host rule, so api.example.com plus /v2 matches https://api.example.com/v2/… and nothing else. Add strip_prefix: true and the edge removes the matched prefix before forwarding, so a backend written to serve from / sees the bare path; the strip happens after your middleware chain has run.
Weighted backends. Instead of a single backend, a route may declare backends — a list of url + weight entries (weight defaults to 1). traefiklinker renders them as a weighted service over per-backend load balancers, and the edge splits traffic proportionally: weights of 9 and 1 send roughly nine requests to the first backend for every one to the second. That makes canary releases and simple load spreading a route-level affair:
name: provision-api-v2-canary
trigger:
event: Fact.System.Boot
filter: { component.eq: playbook-service }
steps:
- run: traefik.register_route
with:
route:
name: api-v2
host: api.example.com
path_prefix: "/v2"
strip_prefix: true
backends:
- url: "http://127.0.0.1:9310"
weight: 9
- url: "http://127.0.0.1:9311"
weight: 1
protocol: https
tls:
cert_resolver: letsencrypt
Only requests to api.example.com under /v2 match this route; nine in ten go to the first backend and the rest to the second, and each backend receives the path with /v2 already stripped.
One verb publishes every kind of route — the protocol field tells the edge how to speak to your backend. traefiklinker supports seven protocols, covering ordinary web traffic, real-time connections, and RPC. For the WebSocket protocols (ws and wss) the edge applies generous idle timeouts automatically, so a connection that sits quietly between messages is not cut off; for SSE and gRPC-Web the daemon attaches the protocol middlewares those streams need — always after any middlewares you declared yourself. You choose the protocol and the rest follows.
| Protocol | What it is for |
|---|---|
http | Plain, unencrypted HTTP — typically only behind another layer, or for local testing. |
https (default) | Standard encrypted web traffic. The everyday choice for serving a page or an API publicly. |
ws | Plain WebSocket — a long-lived, bidirectional connection for live updates. Long idle timeouts are applied automatically. |
wss | WebSocket over TLS — the encrypted form, for real-time dashboards on the public internet. Long idle timeouts are applied automatically. |
sse | Server-Sent Events — a one-way stream of updates from server to browser. The required stream headers are attached automatically. |
grpc | gRPC — high-performance RPC between services. |
grpc-web | gRPC-Web — gRPC adapted to run from a browser. The conversion middleware is attached automatically. |
Configuration lives in the daemon’s application.toml. The [redis] block points at the dedicated Redis instance that holds the daemon’s state; [healthcheck] exposes the local health and metrics server; and the [traefik] block is what makes this daemon special — it tells traefiklinker where to write the edge’s dynamic configuration and how to reach the edge’s admin API for the live view used by list_routes. An optional [otel] block enables distributed tracing.
# /opt/binions/traefiklinker-service/config/application.toml
[redis]
port = 6393
password_file = "/etc/binions/secrets/traefiklinker-redis.pass"
[healthcheck]
listen_addr = "127.0.0.1:9103"
# How traefiklinker drives the edge
[traefik]
dynamic_dir = "/opt/binions/traefiklinker-service/dynamic" # routes are written here
api_base_url = "http://127.0.0.1:8082" # edge admin API, for the live view
# Optional: send traces to a collector for end-to-end visibility
# [otel]
# endpoint = "http://127.0.0.1:4317"
traefik.dynamic_dir — the directory the daemon writes route definitions into. The Traefik edge watches this directory as its file provider, so every route registered through a playbook lands here and is picked up automatically.traefik.api_base_url — the edge’s admin API. traefik.list_routes queries it to add a best-effort live view alongside the routes it finds on disk.healthcheck.listen_addr — the local address that answers liveness and readiness probes and serves metrics.redis — the daemon’s own private state store; the password is read from a file, never written inline.As with every daemon, secrets are referenced by file rather than written into the config — see Secrets & credentials. The edge itself, its entry points, and how certificate resolvers are set up are covered in Platform infrastructure and TLS & certificates.
traefiklinker emits a family of Fact.Traefik.* events. You ask it to do something with an Action, and it announces the outcome with a Fact your playbook can wait on — including explicit failure facts, so a route that could not be published is visible rather than silent.
| Event | Meaning |
|---|---|
Fact.Traefik.RouteRegistered | A route was written and is live at the edge. The success signal for traefik.register_route. |
Fact.Traefik.RouteUnregistered | A route was removed; the edge no longer serves that hostname. |
Fact.Traefik.Reconfigured | The dynamic configuration was rewritten — emitted when the set of routes changes. |
Fact.Traefik.Routes | The reply to traefik.list_routes, carrying the merged on-disk and live view of managed routes. |
Fact.Traefik.RouteRegisterFailed | A route could not be registered — an invalid name, a backend that is not a valid URL, or a middleware that fails validation: a malformed CIDR range, a bad users entry, an invalid rate-limit average, or an unknown middleware field. |
Fact.Traefik.RouteUnregisterFailed | A route could not be removed — for example no route by that name exists. |
Every event rides in the platform’s standard envelope with a correlation id, so registering a route, the edge picking it up, and the first request it serves can all be followed as one thread. For the full anatomy of that envelope, see The event envelope.
The daemon runs as binions-traefiklinker.service under a dedicated, unprivileged service account, alongside its own redis-binions-traefiklinker.service which holds its state. Bring both up together:
sudo systemctl enable --now redis-binions-traefiklinker binions-traefiklinker
systemctl status binions-traefiklinker
It is a Type=notify unit running in a hardened sandbox with a watchdog: it must report liveness within its watchdog window or systemd restarts it. The platform validates the configuration file before the daemon is allowed to start, so a typo in your TOML is caught immediately rather than after deploy. Check health directly over the local endpoint:
curl -s http://127.0.0.1:9103/health/ready
curl -s http://127.0.0.1:9103/health/live
curl -s http://127.0.0.1:9103/metrics
If a route does not come up. A registered route that never starts answering almost always means the edge itself is not running or healthy — traefiklinker writes the configuration, but the Traefik edge is what serves it. Confirm the edge is up (see Platform infrastructure), check that the
backendURL is reachable from the host and is a validhttp://orhttps://address, and look for a recentFact.Traefik.RouteRegisterFailedevent explaining why the route was rejected — it names the offending field, including middleware validation errors.