app.example.com, login.example.com {
reverse_proxy 127.0.0.1:3000 {
health_uri /readyz
health_interval 10s
}
}Reverse proxy
Gate One protects applications as a reverse proxy, but it should itself sit behind a front reverse proxy or ingress controller in most deployments (Traefik, nginx, Caddy, a cloud load balancer).
Why
Gate One listens on plain HTTP and does not manage certificates. The ingress terminates TLS, renews certificates and applies HSTS.
One public entry point gives OIDC redirect URIs, cookie domains and MCP metadata a stable URL that is independent of the container address.
The ingress load-balances and health-checks the Gate One replicas, so the container port never has to be published.
WAF rules, rate limits, request-size limits and access logs stay in front of Gate One instead of competing with its own route policy.
Only the ingress needs to know where Gate One runs; clients cannot reach it directly.
Forwarded headers
Gate One reads the incoming scheme, host and X-Forwarded-* headers when
it rewrites backend redirects and computes rate-limit identities. The
ingress must set:
X-Forwarded-Proto— the client scheme;X-Forwarded-Host— the public host;X-Forwarded-For— the client address chain.
Because those headers are trusted from the immediate hop, Gate One must not be reachable directly by clients. Publish only the ingress.
Minimal configurations
Three complete examples, smallest first. Each terminates TLS, passes the
public host, sets X-Forwarded-* and streams responses. The files live in
examples/reverse-proxy/
(Caddy, Traefik and nginx), next to a validate.sh that parses them with
the real implementations.
| Ingress | What you must configure | Handled for you |
|---|---|---|
Caddy | the hostnames | TLS, forwarded headers, streaming, Host |
Traefik | entrypoints, ACME, one router and one service | forwarded headers, streaming, WebSocket upgrades, Host, active health check |
nginx | everything below | nothing — each line is explicit |
Caddy
TLS is automatic. Both hosts are served by the same Gate One, which
dispatches on Host.
Traefik
Static configuration (traefik.yml):
entryPoints:
web: { address: ":80" }
websecure: { address: ":443" }
certificatesResolvers:
le:
acme:
email: admin@example.com
storage: /data/acme.json
httpChallenge: { entryPoint: web }
providers:
file: { filename: /etc/traefik/traefik-dynamic.yml }Dynamic configuration (traefik-dynamic.yml):
http:
routers:
redirect:
rule: "Host(`app.example.com`) || Host(`login.example.com`)"
entryPoints: [web]
middlewares: [to-https]
service: gate-one
gate-one:
rule: "Host(`app.example.com`) || Host(`login.example.com`)"
entryPoints: [websecure]
service: gate-one
tls: { certResolver: le }
middlewares:
to-https:
redirectScheme: { scheme: https, permanent: true }
services:
gate-one:
loadBalancer:
servers:
- url: "http://127.0.0.1:3000"
healthCheck:
path: /readyz
interval: 10s
timeout: 3s127.0.0.1 assumes Traefik shares the host network; in Docker use the
service name (http://gate-one:3000). The label-based Docker form — the
whole runnable stack — is
deploy/vps/.
nginx
# http block
map $http_upgrade $connection_upgrade { default upgrade; '' ''; }
upstream gate_one {
server 127.0.0.1:3000 max_fails=2 fail_timeout=5s;
keepalive 16;
}
server {
listen 80;
server_name app.example.com login.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2; # nginx >= 1.25: "listen 443 ssl;" + "http2 on;"
server_name app.example.com login.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
client_max_body_size 100m; # Gate One proxies uploads
location / {
proxy_pass http://gate_one;
proxy_http_version 1.1;
proxy_set_header Host $host; # routes are keyed on (host, path)
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off; # SSE and long streams
proxy_read_timeout 1h;
proxy_send_timeout 1h;
gzip off;
proxy_redirect off;
}
}Pitfalls
| Mistake | Symptom | Fix |
|---|---|---|
| every route 404: Gate One matches on the public host |
|
| login redirects to | send |
Response buffering on | SSE stalls until the buffer fills (small events seem lost) |
|
Default body limit (nginx 1m) | uploads answer |
|
Public URL is | cookies are | make both |
Gate One reachable directly | a client forges | loopback or internal network only |
Only passive health checks (nginx OSS) | a degraded replica keeps receiving traffic | probe |
Health probes
The ingress checks /healthz (liveness) and /readyz (readiness,
including providers and usage) on each replica.
Read next
Architecture — request path and deployment
Redis — shared state behind the replicas