Examples
Cookbook of fragments valid against the schema, with the key reference in Configuration. Those that include [listener.web] and its backends can be validated as is; the others are added to an existing configuration.
TLS proxy to two backends with health check
schema_version = 1
[listener.web]
listen = "0.0.0.0:443"
mode = "http"
backend = "api"
[listener.web.tls]
cert = "certs/fullchain.pem"
key = "certs/privkey.pem"
alpn = ["h2", "http/1.1"]
min_version = "1.2"
[backend.api]
servers = [
{ addr = "10.0.0.11:9000", weight = 3 },
{ addr = "10.0.0.12:9000", weight = 1 },
]
[backend.api.health_check]
type = "http"
interval = "2s"
timeout = "1s"
fall = 3
rise = 2
http_path = "/healthz"
expect_status = "200-299"
Three failed probes take the server out of the distribution; two successes bring it back. The 3:1 weights send three out of every four requests to the first.
Least-conn balancing
[backend.api]
balance = "leastconn"
servers = [
{ addr = "10.0.0.11:9000", weight = 2 },
{ addr = "10.0.0.12:9000", weight = 1 },
{ addr = "10.0.0.13:9000", weight = 1, backup = true },
]
slow_start = "30s"
leastconn chooses by active connections divided by weight; slow_start raises the weight of a recovered server for 30 s, and the backup only enters when no eligible primary remains.
Connection limits
[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "api"
maxconn = 2048
[listener.web.limits]
max_head_bytes = 16384
max_headers = 100
max_body_bytes = 1048576
[backend.api]
servers = [
{ addr = "10.0.0.11:9000", maxconn = 256 },
{ addr = "10.0.0.12:9000", maxconn = 256 },
]
maxqueue = 128
timeout_queue = "500ms"
At the listener's cap the newcomer is rejected; at a server's cap, the balancer stops choosing it. If all of them reach it, the request waits in the queue and, once timeout_queue is exhausted, 503 is returned.
Response cache
[listener.web.cache]
default_ttl = "60s"
max_memory_bytes = 67108864
max_entry_bytes = 1048576
disk_path = "cache/web"
GETs with statuses 200, 301, 308, 404 and 410 and without Set-Cookie are stored; requests with Cookie, Authorization, Range or no-store are neither looked up nor stored. The response carries X-Cache: HIT, MISS or REVALIDATED.
Rate limit per rule
[[listener.web.rules]]
name = "api"
host = "api.example.com"
path_prefix = "/api/"
use_backend = "api"
rate_limit = { rate = 10, per = "1s", burst = 20 }
rate = 10 with per = "1s" is 10 requests per second; with per = "60s", 10 per minute. Without burst, the bucket has capacity rate. With several instances, the limit is per instance.
Path-based access to different backends
[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "web"
[[listener.web.rules]]
name = "api-v1"
path_prefix = "/api/v1/"
use_backend = "api"
[listener.web.rules.rewrite]
path_prefix_rewrite = "/api/"
[[listener.web.rules]]
name = "admin-writes"
path_prefix = "/admin/"
methods = ["POST", "PUT", "DELETE"]
use_backend = "admin"
[[listener.web.rules]]
name = "healthz"
path = "/healthz"
[listener.web.rules.direct_response]
status = 200
content_type = "text/plain"
body = "ok"
[backend.web]
server = "10.0.0.21:8080"
[backend.api]
server = "10.0.0.22:9000"
[backend.admin]
server = "10.0.0.23:9000"
/api/v1/users reaches api as /api/users; /healthz is answered locally; everything else falls to web. Order matters, and path_prefix does not normalize: /api/ also matches longer paths.
Docker deployment with Compose
services:
eqproxy:
image: ghcr.io/exyonq/eqproxy:0.9.1
command: ["run", "--config", "/etc/eqproxy/eqproxy.toml"]
ports: ["8080:8080", "127.0.0.1:9001:9001"]
volumes:
- ./eqproxy.toml:/etc/eqproxy/eqproxy.toml:ro
- ./admin.token:/etc/eqproxy/admin.token:ro
- ./certs:/certs:ro
- eqproxy-state:/var/lib/eqproxy
depends_on: [app1, app2]
restart: unless-stopped
app1:
image: ghcr.io/acme/storefront:1.0
expose: ["9000"]
app2:
image: ghcr.io/acme/storefront:1.0
expose: ["9000"]
volumes:
eqproxy-state:
schema_version = 1
[admin]
listen = "127.0.0.1:9001"
token_file = "/etc/eqproxy/admin.token"
[listener.web]
listen = "0.0.0.0:8080"
mode = "http"
backend = "app"
[backend.app]
servers = [
{ addr = "app1:9000", weight = 1 },
{ addr = "app2:9000", weight = 1 },
]
[backend.app.health_check]
type = "tcp"
interval = "2s"
fall = 3
rise = 2
Service names are resolved by DNS in the background, without blocking the listeners. The mounted file must be readable by uid 65532, and the state volume keeps the config apply history. To reload hot, edit the TOML and send the signal: docker kill --signal=HUP eqproxy.