Skip to content
Talk to our solutions team

Gateway Usage

The kis.ai API gateway (binary gateway.svc) is a TLS-terminating, multi-tenant edge proxy and static/SPA file server. It embeds Traefik v3 in process and translates a kis.ai-flavoured YAML config into Traefik’s dynamic configuration, adding CEPT (tenant) resolution, header/path transforms, and metric/telemetry middleware on top.

This guide is the quickstart and the how-to. Every example named below is embedded in the binary, and gateway.svc config generate <name> prints it.

Local dev, HTTP-only, no certs, runs without root:

Terminal window
# 1. Bootstrap file (listeners, log level, where routing lives)
gateway.svc config generate quickstart > .gateway.yaml
# 2. A routing config, dropped into the config/ directory
gateway.svc config generate spa-http -d config/
# 3. Content for the SPA example to serve
mkdir -p public && echo '<h1>hello gateway</h1>' > public/index.html
# 4. Run it
gateway.svc -f .gateway.yaml -L debug
# 5. Hit it
curl http://localhost:8443/

See what else ships:

Terminal window
gateway.svc config list # every embedded example + one-liner
gateway.svc config generate single-app # print one to stdout
gateway.svc config generate common -d config/ # write config/common.yaml
gateway.svc config generate all -d config/ # write every example

For production, start from bootstrap instead of quickstart (port 443, zerotrust: true).

The gateway reads two kinds of configuration, and they are kept separate:

KindWhat it isWhere it comes from
Boot / service configListeners, log level, TLS posture, datacenter/cluster, and the pointer to the routing configThe -f bootstrap file, plus the chassis config: block (local folder or remote config.svc)
Routing configdomainmaps, backends, endpoints, routes, staticpaths. The gateway’s own data modelInline in the bootstrap, or a local directory (config.router.dir), or served by config.svc (config.url)

The config: block selects how the service config is sourced (this is the fleet-wide chassis convention, same keys as every other kis.ai service):

config:
url: http://config:5011 # REMOTE: fetch service+cluster config from config.svc
apikey: <api-key> # auth for the remote config service (optional)
cache: /var/lib/gateway/cfg.db # bbolt warm-start cache (optional)
# path: ./service-config # LOCAL service-config folder (alternative to url)
router:
dir: ./config # LOCAL gateway ROUTING config directory (see below)
  • config.url → remote mode. config.svc serves the service config and the routing model (the same top-level domainmaps/routes/backends/… keys), streamed over SSE and hot-reloaded.
  • config.router.dir → local mode, a directory of routing YAML files in the gateway’s multi-file format, loaded and hot-reloaded by the gateway itself.
  • Neither, with routing inline in the bootstrap → “bootstrap-as-config”; the -f file is the whole config. (This is what most of the embedded single-file examples do.)

Note, config.router.dir vs config.path. config.path is the chassis’s service-config folder (lib-config flat-path format). The gateway’s routing directory is a different format and uses its own key, config.router.dir. Don’t put routing files under config.path, the chassis will try to parse them as service config and fail.

There is no --config_dir flag: the routing directory is the config.router.dir key in the bootstrap file.

name: API Gateway # service name (optional)
log: stdout # stdout | host:port (fluentd)
loglevel: info # info | debug | warn | error
host: "" # bind address; "" = all interfaces
port: 8443 # public entrypoint (443 in prod)
zerotrust: false # true = TLS termination + mTLS on the internal entrypoint
internal:
port: 8444 # localhost-only entrypoint (required)
trustedips: [127.0.0.1] # trusted proxies (applied when zerotrust: true)
timeouts: # gateway-wide listener timeouts (per-entrypoint overridable)
read: 10s
write: 10s
idle: 180s

A request flows through five named concepts:

Host header ──► domainmap ──► (Customer, Product, Environment, Tenant)
│
route ── binds a domainmap to an entrypoint and a list of ──┐
│ │
endpoints ──► backend (a pool of URLs) │
│ │
staticpaths ──► a folder served by a spawned fileserver
│
entrypoint (a listener: websecure / websecureinternal / custom)
  • domainmaps map one or more Host values to a CEPT tuple. The customer-domain-mapper middleware resolves the incoming host and injects X-Customer, X-Product, X-Env, X-Tenant headers so backends know which tenant a request belongs to.
  • backends are named pools of upstream URLs.
  • endpoints match a path prefix and forward to a backend, applying an ordered middleware chain.
  • routes tie a domainmap + an entrypoint to a set of endpoints and/or staticpaths.
  • entrypoints are the listeners. With no explicit entrypoints: block the gateway synthesises two from the boot keys: websecure (:{port}) and websecureinternal (:{internal.port}).

How the routing domain is resolved (running behind a proxy)

Section titled “How the routing domain is resolved (running behind a proxy)”

The routing domain comes from the request’s own Host/authority, a port-less Host on the standard ports (:443/:80) resolves correctly, an incoming X-Forwarded-Host is trusted only from a peer listed in that entrypoint’s trustedips; from any other source it is ignored (and all X-Forwarded-* are stripped), so a client can’t choose the routing domain, or force an SNI/route mismatch, by spoofing the header.

A direct-to-internet gateway needs nothing extra: clients connect with the real Host, and that’s what routing uses. But if you run the gateway behind a reverse proxy or load balancer that terminates the client connection and forwards the real host in X-Forwarded-Host (e.g. kong, which sends Host: localhost), you must list that proxy’s IP in the entrypoint’s trustedips, otherwise the gateway routes by the proxy’s Host (e.g. localhost) and nothing matches:

entrypoints:
- name: websecure
address: ":443"
protocol: http
trustedips: [10.0.1.100] # the upstream proxy/LB; its X-Forwarded-Host is honored
# gateway.svc config generate single-app
backends:
- backend:
name: svc
urls:
- http://127.0.0.1:3000
endpoints:
- name: to-svc
path: { prefix: / }
backend: svc
middlewares:
- customer-domain-mapper: { default: true }
routes:
- name: main
domainmap: main
entrypoint: websecure
endpoints: [to-svc]
domainmaps:
- name: main
domains: [myapp.kisai.dev]
customer: kisai
product: myapp
environment: prod
tenant: main

Every request to myapp.kisai.dev is forwarded to http://127.0.0.1:3000, and the backend receives X-Customer: kisai, X-Product: myapp, X-Env: prod, X-Tenant: main.

Several services behind one domain (path prefixes)

Section titled “Several services behind one domain (path prefixes)”

Route different path prefixes to different backends, stripping the prefix before forwarding:

backends:
- backend: { name: config, urls: [https://127.0.0.1:7123] }
- backend: { name: janus, urls: [https://127.0.0.1:7101] }
- backend: { name: seshat, urls: [https://127.0.0.1:7102] }
endpoints:
- name: to-config
path: { prefix: /config/ }
backend: config
middlewares:
- replaceRegexPrefix: { regex: "^/config/(.*)", replacement: "/${1}" }
- customer-domain-mapper: { default: true }
- name: to-janus
path: { prefix: /account/ }
backend: janus
middlewares:
- replaceRegexPrefix: { regex: "^/account/(.*)", replacement: "/${1}" }
- customer-domain-mapper: { default: true }
- name: to-seshat
path: { prefix: /data/ }
backend: seshat
middlewares:
- replaceRegexPrefix: { regex: "^/data/(.*)", replacement: "/${1}" }
- customer-domain-mapper: { default: true }
routes:
- name: main
domainmap: main
entrypoint: websecure
endpoints: [to-config, to-janus, to-seshat]

replaceRegexPrefix rewrites /config/foo → /foo before the request reaches the config backend. (The slice-2 rewrite: block, below, is the newer way to express the same thing.)

Give each tenant its own subdomain and domainmap; share the backends and endpoints:

# gateway.svc config generate multi-tenant
routes:
- name: tenant-a
domainmap: tenant-a
entrypoint: websecure
endpoints: [to-config, to-janus, to-seshat]
- name: tenant-b
domainmap: tenant-b
entrypoint: websecure
endpoints: [to-config, to-janus, to-seshat]
domainmaps:
- name: tenant-a
domains: [tenant-a.forge.kisai.dev]
customer: kisai
product: forge
environment: prod
tenant: tenant-a
- name: tenant-b
domains: [tenant-b.forge.kisai.dev]
customer: kisai
product: forge
environment: prod
tenant: tenant-b

The same backend pool serves both tenants; the customer-domain-mapper stamps the right X-Tenant per request so the backend can isolate data.

backends:
- backend:
name: api
urls:
- http://10.0.0.1:8080
- http://10.0.0.2:8080 # >1 URL ⇒ round-robin by default
https: true # upgrade to https when zerotrust:true (default true)
profile: sticky-session # opt into a named load-balance profile (see below)

The gateway can serve a folder directly: it spawns its own small file server on the URL you give and proxies the route to it. Two types:

typeBehaviour
staticServes files as-is. Missing file → notfoundpath (with 404) if set, else plain 404.
spaSame, but a missing path falls back to notfoundpath (your index.html) so the SPA’s client-side router can take over. This is what makes deep links work.
# gateway.svc config generate spa
staticpaths:
- name: build
type: spa
folderpath: /var/www/app # your built output (Vite ./dist, CRA ./build, …)
notfoundpath: index.html # SPA fallback — serve index.html for unknown paths
indexpath: index.html
path: { prefix: / }
services:
- url: http://localhost:7151 # gateway spawns its fileserver on this local port
middlewares:
- customer-domain-mapper: { default: true }
routes:
- name: main
domainmap: main
entrypoint: websecure
staticpaths: [build] # note: staticpaths, not endpoints

Build your SPA with the correct base path (base: '/' for Vite, "homepage": "." for CRA), or deep links and asset URLs will break under a sub-path.

spa-http pairs with the quickstart bootstrap, HTTP-only, catch-all on /, matches localhost/127.0.0.1, no common.yaml needed:

# gateway.svc config generate spa-http
staticpaths:
- name: site
type: spa
folderpath: ./public
notfoundpath: index.html
indexpath: index.html
path: { prefix: / }
services:
- url: http://localhost:7150
middlewares:
- customer-domain-mapper: { default: true }
routes:
- name: local
domainmap: local
entrypoint: websecure # plain HTTP when zerotrust:false
staticpaths: [site]
domainmaps:
- name: local
domains: [localhost, 127.0.0.1]
customer: kisai
product: spahttp
environment: dev
tenant: local

Plain static site (Hugo / MkDocs / Docusaurus)

Section titled “Plain static site (Hugo / MkDocs / Docusaurus)”

Use type: static and point notfoundpath at your 404.html, gateway.svc config generate static-site.

The spawned fileserver binds to the services[].url port on localhost. The gateway proxies the route to it; the route’s middleware chain (auth, mapper, metrics, …) runs in the gateway, in front of the fileserver.

URL rewrites, redirects & header transforms

Section titled “URL rewrites, redirects & header transforms”

These are endpoint-level blocks (rewrite:, redirect:, headers:, security_headers:): gateway.svc config generate url-rewrite.

endpoints:
# strip a prefix: /api/v1/users → /users
- name: strip
path: { prefix: /api/v1 }
backend: api-svc
rewrite:
strip_prefix: /api/v1
# strip then add: /api/v1/users → /internal/users
- name: combo
path: { prefix: /api/v1 }
backend: internal-svc
rewrite:
strip_prefix: /api/v1
add_prefix: /internal
# regex with capture groups: /v1/users/42 → /v2/u/42
- name: regex
path: { prefix: /v1/users }
backend: users-svc
rewrite:
path:
match: ^/v1/users/(\d+)$
replace: /v2/u/$1
# rewrite the Host header the backend sees
- name: host
path: { prefix: /vendor }
backend: vendor-svc
rewrite:
strip_prefix: /vendor
host: vendor.partner.com

rewrite.path (static or {match, replace}) cannot be combined with strip_prefix/add_prefix on the same endpoint.

Redirect (no backend: terminates the chain)

Section titled “Redirect (no backend: terminates the chain)”
endpoints:
- name: legacy
path: { prefix: /old }
redirect:
from: ^/old/(.*)$ # default ^.*$
to: /new/$1
permanent: true # 308 (true) vs 302 (false)

redirect: is mutually exclusive with backend: and with rewrite:.

endpoints:
- name: headers
path: { prefix: /augmented }
backend: app-svc
headers:
request:
set:
X-Api-Version: "v1"
X-Source: gateway
remove: [Cookie, Authorization]
response:
set:
Cache-Control: "no-store"
remove: [Server, X-Powered-By]
endpoints:
- name: secure
path: { prefix: /app }
backend: app-svc
security_headers: strict # strict | relaxed | none
  • strict: HSTS (2y, preload), frameDeny, nosniff, XSS filter, Referrer-Policy, CSP default-src 'self'.
  • relaxed: HSTS (1y), X-Frame-Options: SAMEORIGIN, nosniff, XSS filter, Referrer-Policy (no CSP).
  • none: emit nothing.

Header values accept ${...} template substitution, resolved per request from a curated namespace, gateway.svc config generate headers-expr. The values are populated by upstream middlewares (customer-domain-mapper, kisaiauth), so put the mapper before the header block.

endpoints:
- name: tenant-fwd
path: { prefix: /api }
backend: api-svc
middlewares:
- customer-domain-mapper: { default: true }
headers:
request:
set:
X-Tenant-Id: "${tenant.customer}"
X-Tenant-Slug: "${tenant.customer}-${tenant.env}"
X-User: "${auth.subject}"
remove: [Authorization]
response:
set:
X-Served-By: "gateway.${gateway.cluster}"
X-Backend-Time: "${upstream.duration_ms}ms"

Reserved namespaces:

NamespaceFields
request.*method, scheme, host, path, path_segments[N], query.<n>, headers.<n>, cookies.<n>, remote_addr, body_size
tenant.*customer, product, env, name
auth.*subject, user_id, realm
response.*status, headers.<n> (response-side only)
upstream.*duration_ms (response-side only)
gateway.*datacenter, cluster, version

A missing variable renders as the empty string; for a header value, empty means “strip the header”. CRLF in rendered values is stripped (request-smuggling defence).

Declare named profiles once; backends opt in via profile:. Backends with more than one URL round-robin by default, gateway.svc config generate load-balancing.

loadbalance:
default: # applied to any backend without an explicit profile
algo: wrr
healthcheck: { path: /health, interval: 30s, timeout: 5s }
profiles:
sticky-session: # cookie-based session affinity
algo: sticky
sticky: { cookie: srv-id, httpOnly: true, secure: true }
weighted-80-20: # static traffic split across backends
algo: weighted
members:
- { backend: prod, weight: 4 }
- { backend: canary, weight: 1 }
canary-10pct: # shadow 10% to a canary (fire-and-forget)
algo: mirror
primary: prod
mirrors: [{ backend: canary, percent: 10 }]
region-failover: # primary + hot standby; health-check drives the flip
algo: failover
primary: us-east
fallback: us-west
healthcheck: { path: /health/deep, interval: 10s, timeout: 3s }
adaptive-cpu-mem: # gateway polls each member and rewrites weights
algo: adaptive
members: [{ backend: pool-a }, { backend: pool-b }, { backend: pool-c }]
reconcile: { interval: 10s, metrics_path: /health/load, timeout: 3s }
formula: cpu_then_memory # cpu_then_memory | inflight_count | available_capacity
min_weight: 1
max_weight: 100
AlgorithmUse
wrrWeighted round-robin (the default).
stickyWRR + cookie session affinity.
weightedStatic split across multiple backends (canary by share).
mirrorPrimary serves; mirrors get a sampled copy, responses discarded.
failoverPrimary with a health-gated fallback.
adaptiveGateway polls each member’s metrics_path every interval and recomputes weights from formula.

For adaptive, each member must expose a JSON endpoint at metrics_path (default /health/load) with CPU/memory/inflight fields. reconcile.interval has a 5s floor. The controller keeps last-known weights after transient probe failures.

Two independent timeout layers can cut a request, and both live under timeouts: in the bootstrap .gateway.yaml. timeouts is a boot-plane key, so it is stripped from routing-dir files, putting it in common.yaml/backend.yaml does nothing (you’ll get a warning in the log).

# .gateway.yaml — these are the defaults
timeouts:
read: 10s # max time to read the request (headers + body)
write: 0s # max time to write the WHOLE response; 0 = no deadline (default)
idle: 180s # max keep-alive idle between requests on a connection

These become Traefik’s respondingTimeouts and apply to every route on the entrypoint.

write defaults to 0s (no deadline), so SSE / WebSockets / streaming work out of the box. If you set a positive write for slow-write protection, know that it’s a hard deadline on the entire response, not an idle timer, it severs any stream at exactly write seconds no matter how much data is flowing (a source emitting an event every 3s is still cut at the 10s mark; heartbeats do not reset it, and Traefik does not exempt streaming). So either leave it 0s, or set it and route streaming endpoints through a dedicated entrypoint that keeps write: 0s (below). read and idle still bound request reads and idle keep-alives regardless.

To keep the cap on normal routes and lift it only for streams, declare an explicit entrypoints: block (gateway.svc config generate multi-port) and override timeouts on the listener the streaming routes use:

entrypoints:
- name: stream
address: ":8446"
protocol: http
timeouts: { write: 0s } # only this listener drops the write deadline

A second layer sets the request-handling context deadline per backend, with per-path regex overrides, also under timeouts: in the bootstrap (or delivered via the remote cluster config from config.svc):

.gateway.yaml
timeouts:
services:
- name: app.orders # the service (backend) name
defaultcontexttimeout: 30s # default context deadline for this service
pathconfig:
- { expr: "^/orders/export/.*", duration: 120s } # slower path, longer deadline
- { expr: "^/orders/stream/.*", duration: 0s } # 0s = no deadline (streaming)

pathconfig entries are matched by expr (Go regexp) against the request path, first match wins. duration: 0s (or enabled: false) removes the deadline for that path. Use it for streaming endpoints.

Reach for the per-service layer only when you need different deadlines per backend or path. For a plain “my SSE dies after 10s”, the listener write: 0s is the whole fix.

External TLS and internal mTLS are two independent planes: don’t conflate them:

  • External (public) TLS: client ↔ gateway on a public entrypoint. A route opts in by declaring certresolver: (a Let’s Encrypt / ACME cert) and/or tlsoptions: / tlspassthrough:. This is independent of zerotrust: a route can serve public HTTPS with an LE cert whether or not the internal mesh is on. Works on both endpoints: and staticpaths: routes.
  • Internal mTLS: zerotrust: true turns on mutual TLS on the websecureinternal entrypoint and the gateway→backend leg (via kisaitlsinternal / the kisai_issuer CA). That’s service-mesh identity, not public serving; you do not need it to get a public HTTPS cert.

Hardened TLS profiles live in tlsoptions (usually in a shared common.yaml, gateway.svc config generate common):

tlsoptions:
- default:
minVersion: VersionTLS12
sniStrict: true
cipherSuites: [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, …]
- kisaitlsinternal:
minVersion: VersionTLS12
clientAuth:
caFiles: [/etc/gateway/certs/kisai_issuer.crt]
clientAuthType: RequireAndVerifyClientCert

default and kisaitlsinternal are protected names. You may add new ones but not redefine these. A route selects one with tlsoptions: <name> (optional; a route with only certresolver: uses the default options automatically).

A public Let’s Encrypt cert, step by step

Section titled “A public Let’s Encrypt cert, step by step”
  1. Declare the resolver + the :80 challenge entrypoint once in the traefik: passthrough (in common.yaml):

    traefik:
    static:
    certificatesResolvers:
    letsencrypt:
    acme:
    storage: /var/lib/gateway/acme.json # writable + persisted across restarts
    httpChallenge: { entryPoint: web }
    entryPoints:
    web:
    address: ":80"
    http:
    redirections:
    entryPoint: { to: websecure, scheme: https, permanent: true }
  2. Point the route at it: no zerotrust required:

    routes:
    - name: main
    domainmap: main
    entrypoint: websecure
    certresolver: letsencrypt
    staticpaths: [build] # or endpoints: [...]
    domainmaps:
    - { name: main, domains: [app.kisai.dev], customer: kisai, product: app, environment: prod, tenant: main }
  3. Issuance requires (all of them, or the challenge fails and the gateway serves a self-signed cert):

    • the route’s domains: publicly resolve to this host;
    • the challenge port is reachable from the internet: :80 for HTTP-01 (the web→websecure redirect does not interfere: Traefik serves /.well-known/acme-challenge/ before redirecting);
    • the storage: (acme.json) path is writable and persisted.
  • HTTP-01 (gateway.svc config generate acme-http01): simplest; needs public DNS A records and inbound :80.
  • DNS-01 (gateway.svc config generate acme-dns01): required for wildcards or behind a CDN. DNS-API credentials go in env vars, never YAML.

Troubleshooting. The browser shows a self-signed / “not trusted” cert. That means no LE cert was obtained and the gateway is serving its default-store cert. Check, in order:

  1. the route actually declares certresolver: and the running binary emits it , grep certResolver in the debug log should show tls.certResolver on your router (if absent, rebuild/redeploy the binary);
  2. an ACME attempt is logged for your domain (grep -i 'acme\|obtain'): if there’s none, the router carries no resolver;
  3. the challenge port (:80) is reachable from outside (curl http://<domain>/.well-known/acme-challenge/probe from another host, connection refused/timeout ⇒ firewall);
  4. acme.json is writable.

For Traefik features the gateway doesn’t surface as first-class fields (ACME, dashboard, custom middlewares, http3, …), drop raw Traefik config under traefik.static / traefik.dynamic, by convention, only in common.yaml.

traefik:
static:
certificatesResolvers:
letsencrypt:
acme:
storage: /var/lib/gateway/acme.json
httpChallenge: { entryPoint: web }
dynamic:
http:
middlewares:
strict-rate-limit: { rateLimit: { average: 100, burst: 200 } }
gzip-all: { compress: {} }

Reference them from a route:

routes:
- name: main
domainmap: main
entrypoint: websecure
certresolver: letsencrypt # → traefik.static.certificatesResolvers
extramiddlewares: [strict-rate-limit, gzip-all] # → traefik.dynamic.http.middlewares
endpoints: [to-svc]

Protected paths (the gateway owns these; writes are rejected at boot):

  • traefik.static: entryPoints.websecure, entryPoints.websecureinternal, providers.kisai, global, log, accessLog.filePath, experimental.plugins/localPlugins, serversTransport.insecureSkipVerify/rootCAs, api.insecure.
  • traefik.dynamic: http.routers, tcp.routers, http.services, tcp.services, tls.options.default, tls.options.kisaitlsinternal.

The Traefik dashboard can be exposed behind kisai-auth, gateway.svc config generate dashboard (never set api.insecure: true).

kisaimetrics:
enabled: true
ipaddr: 127.0.0.1
port: 7011 # default 8088

Serves Prometheus metrics at /metrics and Go profiling at /debug/pprof/* on its own listener.

tracked:
cookiename: kisaitracker
expiry: 2160h # 90 days
anonymous:
enabled: true
publishhostport: localhost:8000
publishuri: /account/user/anonuser
excludedpaths: [/health, "/\\.well-known/.*"]
accesslogs:
enabled: true
publishhostport: localhost:8000
publishuri: /telemetry/access

The tracker middleware assigns an anonymous cookie and publishes new-tracker and access-log events asynchronously (fire-and-forget); it is disabled on internal (websecureinternal) traffic. Non-fatal if the publish endpoint is unset.

When config.router.dir points at a folder, every *.yaml in it is merged:

  • List keys (routes, domainmaps, backends, endpoints, staticpaths, tlsoptions) concatenate across files.
  • Everything else is last-wins.

Each file declares a namespace:; names in that file (and bare same-file references) get prefixed with <namespace>., so two files can both define a main route without colliding. Qualified references (common.to-config) cross files and are left as-is. tlsoptions are never namespaced, define them once in common.yaml and reference by plain name.

config/forge.yaml
namespace: forge
routes:
- name: tenant-a # → forge.tenant-a
domainmap: tenant-a # → forge.tenant-a (same-file, auto-prefixed)
entrypoint: websecure
endpoints:
- common.to-config # qualified → left as-is
- to-arkabot # bare → forge.to-arkabot
tlsoptions: kisaitlsinternal

The loader fails fast at boot on: duplicate names in a list; a list key with a non-list value; unresolved references (route→domainmap/endpoint/staticpath, endpoint→backend); an unknown certresolver; or a write to a protected passthrough path, a misconfigured deploy never silently serves half its routes. See config.example/ for a complete multi-file layout.

gateway.svc [flags] # run the gateway
gateway.svc config list # list embedded config examples
gateway.svc config generate <name> # print an example (or -d <dir> to write it)
gateway.svc config generate all -d <dir> # write every example
gateway.svc version # print the build version

Flags (all also settable in the bootstrap file where applicable):

FlagMeaning
-f, --config <file>Bootstrap file (default .gateway.yaml)
-l, --logstdout or host:port
-L, --loglevelinfo|debug|warn|error
-p, --portOverride the public port
--datacenter, --clusterDeployment tags (default default)
--config_url, --config_apikeyRemote config service (folds into the config: block)
--servicediscovery_url, --servicediscovery_apikeyService-discovery registration
--svccert, --svckey, --rootcaTLS material for zero-trust
--zerotrusttrue|false
--jwt_public_keyJWT verification key path
--sidService instance id

See Operations for bootstrap, hot reload, observability, and failure modes.

  • Entrypoints: listeners, TCP and UDP, per-listener timeouts
  • Load balancing: profiles in full, including adaptive weights
  • Passthrough: raw Traefik configuration and the protected paths
  • Security: the edge posture and a review checklist