Migrating from Traefik

Sōzune reads the same kind of container labels as Traefik, with a flatter shape. This page maps the Traefik labels you most likely use to their Sōzune equivalent.

The model in one example

Traefik splits a route into a router, a service and named middlewares, linked by name:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.api.rule=Host(`api.example.com`) && PathPrefix(`/v1`)"
  - "traefik.http.routers.api.entrypoints=websecure"
  - "traefik.http.routers.api.tls.certresolver=letsencrypt"
  - "traefik.http.routers.api.middlewares=api-strip,api-ratelimit"
  - "traefik.http.middlewares.api-strip.stripprefix.prefixes=/v1"
  - "traefik.http.middlewares.api-ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.api-ratelimit.ratelimit.burst=50"
  - "traefik.http.services.api.loadbalancer.server.port=8080"

Sōzune has one level: every setting of a route sits under sozune.http.<svc>., middlewares included.

labels:
  - "sozune.enable=true"
  - "sozune.http.api.host=api.example.com"
  - "sozune.http.api.path=/v1"
  - "sozune.http.api.tls=true"
  - "sozune.http.api.acme.resolver=letsencrypt"
  - "sozune.http.api.httpsRedirect=true"
  - "sozune.http.api.stripPrefix=true"
  - "sozune.http.api.ratelimit.average=100"
  - "sozune.http.api.ratelimit.burst=50"
  - "sozune.http.api.port=8080"

No rule language, no middleware names to keep in sync, no entrypoint to pick for plain HTTP or HTTPS routes. The letsencrypt resolver has to be declared under acme.resolvers in Sōzune's config, like the Traefik certificate resolver in its static config.

Discovery

TraefikSōzune
traefik.enable=truesozune.enable=true
traefik.docker.network=<name>sozune.network=<name>
providers.docker.exposedByDefaultexpose_by_default on the provider. Traefik defaults to true, Sōzune to false: add sozune.enable=true to every container to expose.

Router rules

A Traefik rule becomes one label per matcher. All the matchers of a route must match.

Traefik matcherSōzune label
Host(`a.example.com`)host=a.example.com
Host(`a.example.com`) || Host(`b.example.com`)host=a.example.com,b.example.com
HostRegexp(`^[^.]+\.example\.com Sozune — The modern reverse proxy )host=*.example.com
HostRegexp(`^cdn[0-9]+\.example\.com Sozune — The modern reverse proxy Migrating from Traefik — Sozune Docs )host=/cdn[0-9]+/.example.com: the regex goes between slashes, and the domain at the end stays written out. A regex that would match any domain is refused. See Regex hostnames.
PathPrefix(`/api`)path=/api. Sōzune matches on segment boundaries: /api serves /api/users but not /apiv2, which Traefik's PathPrefix does. pathRegex=^/api keeps that broader match, but stripPrefix only applies to path.
PathRegexp(`^/users/[0-9]+`)pathRegex=^/users/[0-9]+. Keep the ^: without it, the regex can match anywhere in the path or in its query string.
Path(`/exact`)Exact paths are only available through the HTTP provider and the API
Method(`GET`)methods=GET (comma-separated for several)
Header(`X-Api-Version`, `2`)matchHeaders=X-Api-Version:2 (see the note below)
Query(`beta`, `1`)matchQuery=beta:1 (see the note below)
ClientIP(`10.0.0.0/8`)matchClientIP=10.0.0.0/8 (see the note below)
routers.<r>.prioritypriority. Traefik ranks routes by rule length when no priority is set. Sōzune does not: at equal priority, the first route by id wins, even over a longer path. Set priority on the more specific route when two routes overlap.

matchHeaders chooses between routes, as Header() does: two routers on the same host and path that differ only by a header translate one to one, and a request without the header goes to the other route. A route whose header conditions hold comes before every route without any, whatever their priority.

matchQuery chooses between routes too, as Query() does, for up to three conditions on a route without pathRegex, stripPrefix or addPrefix. matchClientIP filters a route, it does not choose between routes: Sōzune picks the route, then answers 404 when the client IP is not listed, without trying another route. Two Traefik routers on the same host and path that differ only by a client IP need distinct paths in Sōzune. See Header & query matching.

A rule that mixes || across different matcher types has no single-route equivalent: declare one service per alternative. Each of them needs a host, so an alternative on a path alone, on any host, cannot be expressed with labels.

TLS and certificates

TraefikSōzune
routers.<r>.tls=truetls=true
routers.<r>.tls.certresolver=<name>tls=true and acme.resolver=<name>. Resolvers are declared under acme.resolvers; without acme.resolver, the certificate is issued over HTTP-01.
routers.<r>.entrypoints=web,websecureNothing: HTTP and HTTPS listeners are global.
routers.<r>.entrypoints=websecure (HTTPS only)tls=true and httpsRedirect=true. A tls=true route is still served over plain HTTP; the redirect sends those requests to HTTPS.
redirectscheme.scheme=httpshttpsRedirect=true (Redirects)
redirectscheme.port=8443httpsRedirectPort=8443
Certificates from files (tls.certificates)proxy.https.tls.certificates

Service

TraefikSōzune
services.<s>.loadbalancer.server.portport
services.<s>.loadbalancer.sticky.cookie=truestickySession=true
services.<s>.loadbalancer.healthcheck.pathhealthCheck.path. A backend that fails it leaves the rotation, unless every backend of the route does. See Health checks.
services.<s>.loadbalancer.healthcheck.timeout=2shealthCheck.timeout=2000 (milliseconds)
services.<s>.loadbalancer.healthcheck.interval=5shealthCheck.interval=5000 (milliseconds)
services.<s>.loadbalancer.healthcheck.porthealthCheck.port
services.<s>.loadbalancer.healthcheck.headers.<Name>healthCheck.headers.<Name>
services.<s>.loadbalancer.healthcheck.unhealthyInterval, followRedirects, scheme, hostname, methodNo equivalent. Thresholds instead: healthCheck.unhealthyThreshold / healthyThreshold.
serverstransport.forwardingtimeouts.responseheadertimeout=2sbackendTimeout=2000 (milliseconds). It caps the whole response, not only the wait for its headers: leave room for a long or streamed body.

Containers that declare the same <svc> name with the same hosts and paths are merged as backends of one route, like replicas of a Traefik service. If their hosts or paths differ, the later container gets a route of its own instead.

Middlewares

In Sōzune, a middleware is a label on the route itself: there is no middlewares= list and no shared middleware definition.

Traefik middlewareSōzune label
stripprefix.prefixes=/api on a router with PathPrefix(`/api`)path=/api + stripPrefix=true. stripPrefix removes the route's own path: a router that strips a prefix it does not match on has no equivalent.
addprefix.prefix=/fooaddPrefix=/foo
headers.customrequestheaders.X-Foo=barheaders.X-Foo=bar
headers.customresponseheaders.X-Foo=barheaders.response.X-Foo=bar
Empty header value (removes the header)Same: headers.X-Foo=
basicauth.users=user:$apr1$...auth.basic=user:<sha256-hex>. Sōzune does not read htpasswd hashes: rehash each password in SHA-256.
basicauth.realmwwwAuthenticate
forwardauth.addressforwardAuth.address
forwardauth.authResponseHeadersforwardAuth.responseHeaders
forwardauth.trustForwardHeaderforwardAuth.trustForwardHeader
ratelimit.average / ratelimit.burstratelimit.average / ratelimit.burst (requests per second, per client IP). Without burst, Traefik allows a burst of 1 and Sōzune a burst equal to average: set ratelimit.burst to keep the Traefik behaviour.
inflightreq.amountinFlightReq, counted per client IP. Traefik groups by request host by default, which caps the whole route; Sōzune has no route-wide cap.
ipallowlist.sourcerangeipAllowList
compress=truecompress=true
retry.attemptsretry.attempts
circuitbreaker.expressioncircuitBreaker.threshold, circuitBreaker.minRequests, circuitBreaker.cooldown. Sōzune trips on an error rate, not on an expression: see Circuit breaker.
errors.status + errors.serviceNo equivalent for errors returned by the backend. errorPages.<status>=<html> only replaces the errors Sōzune answers itself (no route, backend down…), with an inline page.
Traefik pluginsWASM plugins. Traefik's Yaegi plugins do not run as is.

Not available yet: chain, digestauth, buffering, redirectregex, replacepath, replacepathregex, mirroring and weighted services. A weighted split between two versions is written with a weight on the containers of one service instead: see Weighted load balancing.

TCP

TraefikSōzune
A TCP entrypoint in the static configA listener under proxy.tcp
tcp.routers.<r>.entrypoints=postgressozune.tcp.<svc>.entrypoint=postgres
tcp.routers.<r>.rule=HostSNI(`*`)Don't set sni: without it, the listener sends every connection to its service.
tcp.routers.<r>.rule=HostSNI(`a.example.com`) + tls.passthrough=truesozune.tcp.<svc>.sni=a.example.com
tcp.services.<s>.loadbalancer.server.portsozune.tcp.<svc>.port

Check the result

Once the labels are rewritten, sozune validate reads them without starting the proxy and reports every unknown or invalid label, with a suggestion for typos. On a running instance, the dashboard's Diagnostics page shows the same, and sozune route <url> tells you which route a request reaches and why.