Traefik with the http provider
Traefik v3 polls a node for dynamic configuration Scafo renders from the fleet's routing. No plugin, no Docker labels, no push.
#Static configuration
A Traefik provider is static configuration, read once at startup. It cannot be declared from a dynamic directory.
providers:
http:
endpoint: "http://scafo-node:7400/v1/ingress/routes/traefik?prefer=self"
pollInterval: 5s
headers:
Authorization: "Bearer <SCAFO_INGRESS_TOKEN>"| Setting | Meaning |
|---|---|
endpoint | The node's Traefik-format routing route. Reach it over loopback, a private network or the tunnel. |
pollInterval | How often Traefik refetches. 5s matches the node's reconcile cadence. |
headers.Authorization | Bearer <token> — the ingress bearer token, the only credential Traefik holds. It never sees bucket credentials. |
Warning
Because the provider is static, the token is written into whatever file or argument list declares it. Treat that file as a secret. On Kubernetes, $(VAR) expansion in container arguments keeps the token in a Secret instead.
#?prefer=
| Query | Effect |
|---|---|
?prefer=self | The node being asked heads every route. Use it for a Traefik colocated with a node. |
?prefer=<node-id> | That node heads every route. |
| (omitted) | The fleet's own order; every request is spread over every upstream. Use it for a Traefik in front of the whole fleet. |
With a preferred node that has peers, the renderer emits a failover service: the preferred node's upstreams while healthy, the others only when they are not. That is more than an ordering, which is why the format needs the node's name rather than just the order.
#EntryPoints
Scafo's routers must attach to entryPoints Traefik actually defines. A router attached to an undefined entryPoint matches nothing.
| Setting | Flag | Environment | Config key | Default |
|---|---|---|---|---|
| EntryPoints | --traefik-entrypoints | SCAFO_TRAEFIK_ENTRYPOINTS | ingress.entryPoints | web |
Repeat --traefik-entrypoints for several entryPoints; SCAFO_TRAEFIK_ENTRYPOINTS separates them with commas, and the config key holds a JSON string array. An empty list is rejected. The node prints the entryPoints its routers attach to at boot.
#What the node renders
For a fleet serving shop.apps.example.com on nodes vps-a and vps-b, with prefer=vps-a:
{
"http": {
"routers": {
"scafo_shop.apps.example.com": {
"rule": "Host(`shop.apps.example.com`)",
"service": "scafo_shop.apps.example.com",
"entryPoints": ["websecure"],
"middlewares": ["scafo_retry"]
},
"scafo_apideny_apps.example.com": {
"rule": "Host(`apps.example.com`) && (Path(`/v1/storage`) || PathPrefix(`/v1/storage/`) || Path(`/v1/ingress/routes`) || PathPrefix(`/v1/ingress/routes/`))",
"service": "noop@internal",
"entryPoints": ["websecure"],
"priority": 3
},
"scafo_api_apps.example.com": {
"rule": "Host(`apps.example.com`) && !Path(`/v1/storage`) && !PathPrefix(`/v1/storage/`) && !Path(`/v1/ingress/routes`) && !PathPrefix(`/v1/ingress/routes/`)",
"service": "scafo_api_apps.example.com",
"entryPoints": ["websecure"],
"middlewares": ["scafo_retry"],
"priority": 2
}
},
"services": {
"scafo_shop.apps.example.com": {
"failover": {
"service": "scafo_shop.apps.example.com_local",
"fallback": "scafo_shop.apps.example.com_peers"
}
},
"scafo_shop.apps.example.com_local": {
"loadBalancer": {
"servers": [{ "url": "http://10.83.0.1:24417" }],
"healthCheck": {
"path": "/.well-known/celld/health",
"interval": "10s",
"timeout": "3s",
"hostname": "shop.apps.example.com"
}
}
},
"scafo_shop.apps.example.com_peers": {
"loadBalancer": {
"servers": [{ "url": "http://10.83.0.2:24902" }],
"healthCheck": {
"path": "/.well-known/celld/health",
"interval": "10s",
"timeout": "3s",
"hostname": "shop.apps.example.com"
}
}
}
},
"middlewares": {
"scafo_retry": { "retry": { "attempts": 3 } }
}
}
}#Naming
| Object | Name |
|---|---|
| Application router and service | scafo_<host> |
| Subdomains of a custom domain claimed with them | scafo_wild_<base> |
| Platform API router and service | scafo_api_<domain> |
| Platform API denial router | scafo_apideny_<domain> |
| Failover halves | <key>_local and <key>_peers |
| Retry middleware | scafo_retry, retry.attempts: 3 |
The scafo_ prefix uses an underscore, which cannot occur in a host, so these names never collide with an operator's own routers in other providers.
#Subdomains of a custom domain
A custom domain claimed with its subdomains reaches the document as one wildcard entry, and the renderer writes it as a HostRegexp router matching exactly one label under the base:
"scafo_wild_shop.example": {
"rule": "HostRegexp(`^[^.]+\\.shop\\.example$`)",
"service": "scafo_wild_shop.example",
"entryPoints": ["websecure"],
"middlewares": ["scafo_retry"],
"priority": 1
}The priority is 1, below every exact Host router this document writes and below the API routers too, so a name somebody claimed exactly always wins over the wildcard enclosing it. It has to be stated: Traefik's default priority is the length of the rule, which would put a long pattern ahead of a short exact host — precisely backwards for a fallback.
The service is built like any other, and its health check carries the base itself as hostname, never the pattern that stands for every name under it.
#Health checks
| Service kind | Path | Interval | Timeout |
|---|---|---|---|
| Application | /.well-known/celld/health | 10s | 3s |
| Platform API | /.well-known/scafo/health | 10s | 3s |
Application checks carry hostname set to the route's host.
#Denied paths
The denial is a separate router at priority 3, matching the denied prefix exactly or followed by /, and pointing at Traefik's internal noop@internal service so it is active without dialling anything. The API router sits at priority 2 and negates the same rules, so a denied path can never reach it.
Both /v1/storage and /v1/ingress/routes are rendered this way, at every platform API host.
#An empty fleet
When the document has no routers at all, the node renders {} — the http key is omitted entirely, which withdraws every route from this provider. Empty maps would instead make Traefik retain a stale configuration, which is why they are never emitted.
Before a node's first reconcile pass completes, the route refuses with 503 and Traefik keeps its previous configuration and logs the error.
#TLS
Scafo's routers carry no TLS block. Termination is Traefik's:
| Setup | How |
|---|---|
| ACME on an entryPoint | Give the entryPoint a default http.tls.certResolver; Scafo's routers inherit it. |
| Kubernetes | Point the entryPoint at a certificate held in a Secret, or let cert-manager issue the platform wildcard. |
| Per-router resolvers already in place | Operator routers that set tls.certresolver themselves still win; nothing already deployed changes. |
Note
Traefik does not use the node's /v1/storage surface. That is Caddy's certificate store; Traefik keeps its own.