Gateway Routing
1. Overview
Routing in BANA uses the Service-Owned Prefix pattern. Each service sets its own base path (/v1/api/<service>) and declares its Traefik route via Docker labels. No central route configuration exists.
Source: Docker labels from infrastructure/deployments/develop/*/docker-compose.yml, static config from packages/gateway/config/traefik.yml
2. Edge → Traefik → Middleware Pipeline
Traffic enters through Nginx (TLS termination), then Traefik receives it on port :30080. Two configuration providers feed Traefik: Docker Provider reads container labels for routers/services, File Provider reads middlewares.yml for shared middleware definitions.
| Component | Source | Details |
|---|---|---|
| Nginx | Edge layer (:80/:443) | TLS termination, adds X-Forwarded-For/X-Real-IP |
| Entrypoint | Traefik web (:80) | Receives HTTP from Nginx on host port :30080 |
| rate-limit@file | middlewares.yml | 200 req/s per-IP, burst: 400, ipStrategy.depth: 1 |
| circuit-breaker@file | middlewares.yml | net error > 10% or P95 latency > 3s (5xx-ratio term commented out) |
| security-headers@file | middlewares.yml | XSS filter, nosniff, frame deny, strip Server header |
| Docker Provider | /var/run/docker.sock | Reads traefik.* labels, auto-creates routers + services |
| File Provider | config/dynamic/middlewares.yml | Shared middlewares, dashboard router + basic auth, hot reload |
3. Router → Service Mapping
After passing through the middleware chain, Traefik matches the request to one of the registered routers. Each router is auto-discovered from Docker labels. The matched router forwards to its corresponding backend service on the Docker network.
| Router | Rule | Middlewares | Notes |
|---|---|---|---|
| identity | PathPrefix(/v1/api/identity) | rate-limit-auth@file, circuit-breaker@file, security-headers@file | Stricter auth rate limit |
| commerce | PathPrefix(/v1/api/commerce) | rate-limit@file, circuit-breaker@file, security-headers@file | |
| sale | PathPrefix(/v1/api/sale) | rate-limit@file, circuit-breaker@file, security-headers@file | |
| finance | PathPrefix(/v1/api/finance) | rate-limit@file, circuit-breaker@file, security-headers@file | |
| inventory | PathPrefix(/v1/api/inventory) | rate-limit@file, circuit-breaker@file, security-headers@file | |
| payment | PathPrefix(/v1/api/payment) | rate-limit@file, circuit-breaker@file, security-headers@file | |
| signal-rest | PathPrefix(/v1/api/signal) | rate-limit@file, circuit-breaker@file, security-headers@file | |
| signal-ws | PathPrefix(/stream) | — | WebSocket passthrough |
| payment-webhook | Host(hook.bana.nexpando.vn) | payment-add-prefix@docker, security-headers@file | priority: 100, path rewrite |
| portal | PathPrefix(/) | dashboard-auth@file | priority: 1, Portal dashboard |
4. Traefik Components
| Component | Source | Description |
|---|---|---|
| Docker Provider | /var/run/docker.sock | Watches running containers for traefik.* labels. Automatically creates routers and services when a container starts, and removes them when it stops. |
| File Provider | config/dynamic/middlewares.yml | Loads shared middleware definitions (rate-limit, circuit-breaker, security-headers, dashboard-auth). Watches the file for changes — edits take effect without restarting Traefik. |
| Router | Docker labels | Matches incoming requests by rule (PathPrefix, Host). Each service container declares its own router. Traefik evaluates rules in priority order (default priority = rule string length). Use explicit priority when multiple routers can match the same request. |
| Middleware | File provider (@file) or Docker labels (@docker) | Processes requests before reaching the backend. Shared middlewares live in file provider and must be referenced with @file suffix from Docker labels. |
| Service | Docker labels | The backend target — Traefik resolves the container IP via Docker network and forwards to the declared port. Each service includes health check configuration. |
| Health Check | Docker labels | Traefik actively polls /v1/api/<service>/health every 30 seconds. If a service fails, Traefik removes it from routing until health is restored. |
5. Request Lifecycle
6. Three Routing Patterns
BANA uses three distinct routing patterns depending on the traffic type:
Pattern 1: Standard REST API
The most common pattern. A single router matches by path prefix and applies the full middleware chain.
Client → Nginx → Traefik
→ Router: PathPrefix(/v1/api/commerce)
→ Middlewares: rate-limit@file → circuit-breaker@file → security-headers@file
→ Service: dev-nx-commerce:3000Used by: identity, commerce, sale, finance, inventory, payment, signal (REST).
Pattern 2: WebSocket (No Middlewares)
WebSocket connections require a persistent connection and must not be interrupted by rate limiting or circuit breaking. The signal-ws router has no middlewares.
Client → Nginx (Connection: upgrade) → Traefik
→ Router: PathPrefix(/stream)
→ Middlewares: (none)
→ Service: dev-nx-signal:3000Traefik natively supports WebSocket — the Connection: Upgrade and Upgrade: websocket headers are forwarded automatically.
Pattern 3: 3rd-Party Webhook with Path Rewrite
For legacy 3rd-party integrations where the registered URL cannot be changed. A Host-based router matches by domain, and a replacepathregex middleware rewrites the path to add the service prefix.
VNPAY → Nginx (Host: hook.bana.nexpando.vn) → Traefik
→ Router: Host(hook.bana.nexpando.vn), priority: 100
→ Middlewares: payment-add-prefix@docker → security-headers@file
→ Path rewrite: /v1/api/payments/* → /v1/api/payment/payments/*
→ Service: dev-nx-payment:3000The payment-add-prefix middleware is defined in Docker labels (not file provider) because it is specific to the payment service only.
Why
priority: 100? Traefik'sPathPrefixis a pure prefix match —PathPrefix(/v1/api/payment)also matches/v1/api/payments/...because/v1/api/paymentis a prefix of/v1/api/payments. Without explicit priority, thepaymentrouter (PathPrefix) andpayment-webhookrouter (Host) have similar default priorities (calculated from rule string length), and Traefik may route the webhook request through the wrong router — skipping the path rewrite. Settingpriority: 100ensures the Host-based webhook router always wins whenHost: hook.bana.nexpando.vnis present.
7. Docker Label Configuration
Standard Service (Identity)
Source: infrastructure/deployments/develop/identity/docker-compose.yml
services:
dev-nx-identity:
labels:
- "traefik.enable=true"
# Router
- "traefik.http.routers.identity.rule=PathPrefix(`/v1/api/identity`)"
- "traefik.http.routers.identity.entrypoints=web"
- "traefik.http.routers.identity.middlewares=rate-limit-auth@file,circuit-breaker@file,security-headers@file"
# Service
- "traefik.http.services.identity.loadbalancer.server.port=3000"
- "traefik.http.services.identity.loadbalancer.healthcheck.path=/v1/api/identity/health"
- "traefik.http.services.identity.loadbalancer.healthcheck.interval=30s"WebSocket Service (Signal)
Source: infrastructure/deployments/develop/signal/docker-compose.yml
Signal service needs two routers — one for REST API, one for WebSocket:
services:
dev-nx-signal:
labels:
- "traefik.enable=true"
# REST API
- "traefik.http.routers.signal-rest.rule=PathPrefix(`/v1/api/signal`)"
- "traefik.http.routers.signal-rest.entrypoints=web"
- "traefik.http.routers.signal-rest.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"
# WebSocket
- "traefik.http.routers.signal-ws.rule=PathPrefix(`/stream`)"
- "traefik.http.routers.signal-ws.entrypoints=web"
# Shared service
- "traefik.http.services.signal.loadbalancer.server.port=3000"
- "traefik.http.services.signal.loadbalancer.healthcheck.path=/v1/api/signal/health"
- "traefik.http.services.signal.loadbalancer.healthcheck.interval=30s"3rd-Party Webhook Router (Payment)
Source: infrastructure/deployments/develop/payment/docker-compose.yml
Payment service has two routers sharing one service. The second router handles 3rd-party webhooks from a legacy domain (hook.bana.nexpando.vn) where the registered URL cannot be changed. A replacepathregex middleware rewrites the path to include the /payment prefix.
services:
dev-nx-payment:
labels:
- "traefik.enable=true"
# Router — normal API traffic
- "traefik.http.routers.payment.rule=PathPrefix(`/v1/api/payment`)"
- "traefik.http.routers.payment.entrypoints=web"
- "traefik.http.routers.payment.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"
# Router — 3rd party webhook (legacy path without /payment prefix)
# Priority 100 ensures this wins over the PathPrefix router when Host matches
- "traefik.http.routers.payment-webhook.rule=Host(`hook.bana.nexpando.vn`)"
- "traefik.http.routers.payment-webhook.priority=100"
- "traefik.http.routers.payment-webhook.entrypoints=web"
- "traefik.http.routers.payment-webhook.middlewares=payment-add-prefix@docker,security-headers@file"
# Middleware — rewrite /v1/api/* to /v1/api/payment/*
- "traefik.http.middlewares.payment-add-prefix.replacepathregex.regex=^/v1/api/(.*)"
- "traefik.http.middlewares.payment-add-prefix.replacepathregex.replacement=/v1/api/payment/$$1"
# Service (shared by both routers)
- "traefik.http.services.payment.loadbalancer.server.port=3000"
- "traefik.http.services.payment.loadbalancer.healthcheck.path=/v1/api/payment/health"
- "traefik.http.services.payment.loadbalancer.healthcheck.interval=30s"Request flow for 3rd-party webhook:
API Portal (Catch-all, lowest priority)
Source: infrastructure/deployments/develop/gateway/docker-compose.yml
The Portal is an Astro + React dashboard that shows service health, OpenAPI endpoints, response times, and system overview. It runs as an nginx container serving the built static output and catches all unmatched routes.
services:
dev-nx-portal:
image: nginx:1.27-alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.portal.rule=PathPrefix(`/`)"
- "traefik.http.routers.portal.entrypoints=web"
- "traefik.http.routers.portal.priority=1"
- "traefik.http.routers.portal.middlewares=dashboard-auth@file"
- "traefik.http.services.portal.loadbalancer.server.port=80"The dashboard-auth@file middleware applies the same HTTP Basic Auth as the Traefik dashboard (user: nx.eventry).
8. Health Check Polling
Traefik actively polls every backend service's health endpoint every 30 seconds. Unhealthy services are removed from routing until they recover. This runs independently from request handling.
9. Auto-Discovery Flow
When a new service container starts with traefik.enable=true, this happens automatically:
No restart of Traefik is required. No central route configuration to update. The service declares its own route and Traefik picks it up within seconds.
10. Middleware Provider Namespacing
Middlewares defined in packages/gateway/config/dynamic/middlewares.yml (file provider) must be referenced with the @file suffix in Docker labels. Without the suffix, Traefik looks for the middleware in the @docker provider and fails silently.
# Correct — references middleware from file provider
- "traefik.http.routers.commerce.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"
# Wrong — Traefik looks for "rate-limit" in Docker provider (doesn't exist)
- "traefik.http.routers.commerce.middlewares=rate-limit,circuit-breaker,security-headers"11. Router Priority & PathPrefix Gotcha
Traefik's PathPrefix is a pure prefix match — it does not enforce a / boundary. This means:
| PathPrefix Rule | Request Path | Matches? |
|---|---|---|
PathPrefix(/v1/api/payment) | /v1/api/payment/webhook | Yes |
PathPrefix(/v1/api/payment) | /v1/api/payments/vnpay/ipn | Yes (no / boundary!) |
PathPrefix(/v1/api/sale) | /v1/api/sale-orders | Yes (same issue) |
When multiple routers match the same request, Traefik picks the one with the highest priority. Default priority is calculated from the rule string length, which can lead to unpredictable results.
When to set explicit priority:
- When a Host-based router overlaps with a PathPrefix router (like
payment-webhookvspayment) - When a catch-all router (like SPA client with
PathPrefix(/)) must have the lowest priority - When two PathPrefix rules overlap (e.g.,
/v1/api/salevs a hypothetical/v1/api/sale-orders)
# Higher number = higher priority (wins)
- "traefik.http.routers.payment-webhook.priority=100" # Wins over PathPrefix routers
- "traefik.http.routers.client.priority=1" # Loses to everything else12. Service Base Path Configuration
Each service sets its base path via environment variable:
# infrastructure/deployments/develop/<service>/.env.development
APP_ENV_SERVER_BASE_PATH=/v1/api/<service>| Service | APP_ENV_SERVER_BASE_PATH |
|---|---|
| identity | /v1/api/identity |
| commerce | /v1/api/commerce |
| sale | /v1/api/sale |
| finance | /v1/api/finance |
| inventory | /v1/api/inventory |
| payment | /v1/api/payment |
| signal | /v1/api/signal |
13. Adding a New Service
- Set
APP_ENV_SERVER_BASE_PATH=/v1/api/<new-service>in the service's.env.development - Add Traefik labels to the service's
docker-compose.yml:yamllabels: - "traefik.enable=true" - "traefik.http.routers.<new-service>.rule=PathPrefix(`/v1/api/<new-service>`)" - "traefik.http.routers.<new-service>.entrypoints=web" - "traefik.http.routers.<new-service>.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file" - "traefik.http.services.<new-service>.loadbalancer.server.port=3000" - "traefik.http.services.<new-service>.loadbalancer.healthcheck.path=/v1/api/<new-service>/health" - "traefik.http.services.<new-service>.loadbalancer.healthcheck.interval=30s" - Add the compose file to
infrastructure/deployments/develop/dcCOMPOSE_FILES array - Traefik auto-discovers the new service — no gateway config changes needed
14. Inter-Service Communication
Services calling each other (e.g., commerce → identity) should NOT go through Traefik. They communicate directly via Docker network:
# Direct container-to-container (does not go through gateway)
APP_ENV_IDENTITY_SERVICE_BASE_URL=http://dev-nx-identity:3000/v1/api/identity
APP_ENV_COMMERCE_SERVICE_BASE_URL=http://dev-nx-commerce:3000/v1/api/commerce15. K8s Migration Path
Docker labels convert mechanically to Traefik IngressRoute CRDs:
# Docker label
traefik.http.routers.identity.rule=PathPrefix(`/v1/api/identity`)
# K8s IngressRoute CRD equivalent
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
spec:
routes:
- match: PathPrefix(`/v1/api/identity`)
services:
- name: identity-service
port: 300016. Related Pages
| Document | Description |
|---|---|
| Gateway Overview | Identity card + service catalog |
| Architecture | C4 views, request-routing flows |
| Middlewares | Rate limiting, circuit breaker, security headers |
| Resilience | Circuit breaker states, health checks, retry |
| Configuration | Local Nginx route table, constants |
| Decisions | ADR-0001 (label routing), ADR-0002 (dev parity) |