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>"
SettingMeaning
endpointThe node's Traefik-format routing route. Reach it over loopback, a private network or the tunnel.
pollIntervalHow often Traefik refetches. 5s matches the node's reconcile cadence.
headers.AuthorizationBearer <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=

QueryEffect
?prefer=selfThe 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.

SettingFlagEnvironmentConfig keyDefault
EntryPoints--traefik-entrypointsSCAFO_TRAEFIK_ENTRYPOINTSingress.entryPointsweb

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

ObjectName
Application router and servicescafo_<host>
Subdomains of a custom domain claimed with themscafo_wild_<base>
Platform API router and servicescafo_api_<domain>
Platform API denial routerscafo_apideny_<domain>
Failover halves<key>_local and <key>_peers
Retry middlewarescafo_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 kindPathIntervalTimeout
Application/.well-known/celld/health10s3s
Platform API/.well-known/scafo/health10s3s

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:

SetupHow
ACME on an entryPointGive the entryPoint a default http.tls.certResolver; Scafo's routers inherit it.
KubernetesPoint the entryPoint at a certificate held in a Secret, or let cert-manager issue the platform wildcard.
Per-router resolvers already in placeOperator 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.

#Recipes

Read more in Deploy > K3s.
Read more in Deploy > Coolify.
Read more in Deploy > Dokploy.