dumbtv
dumbtv가 ADB로 Fire TV에서 광고·추적·업데이트 알림을 제거, 루트·앱 변경 없이, 무료 오픈소스
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기async python load balancer / HTTP1,2,3 / Python 3.15.0t / debian trixie slim
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗An async HTTP(S) load balancer and reverse proxy built on Quart. It fronts a pool of backends with round-robin or sticky routing, health checks with a circuit breaker, per-IP rate limiting, a libmodsecurity-powered WAF, URL rewriting, Prometheus metrics, and a live management web UI.
apython_lb was built on Quart because it is a low-cost (in engineering time) way to get a fully async application off the ground. Quart is an industry-standard, Flask-compatible ASGI framework, so the patterns are familiar and the ecosystem is mature — there is no bespoke runtime to learn or maintain.
The biggest win is Quart's @app.route decorator. It turns URL configuration
into a first-class, declarative concern: routes, path parameters, and HTTP
methods are parsed and dispatched by the framework, which lets apython_lb layer its
own behavior (load balancing, rate limiting, the WAF, URL rewriting) on top as
composable decorators. Instead of hand-rolling a request router, apython_lb spends
its engineering budget on the proxy and traffic-management logic that actually
matters.
h3 extra, alongside
HTTP/1.1 and HTTP/2 on the same TLS certificate.429 +
Retry-After responses.url_match/url_rewrite
regex patterns.LB_MAX_ATTEMPTS). Non-idempotent requests (POST) are only retried when the
failure happened before the request was sent (connect errors) — an ambiguous
failure mid-request is never replayed. A background health checker
independently re-checks all backends and restores them to the active list
once they recover./metrics (via
prometheus-client): request counts, latency histograms, and in-progress
gauges per endpoint, plus backend selections/retries/errors, healthy-server
counts, rate-limit rejections, 404-tracker penalties, and WAF
allowed/blocked inspections. All metric definitions live in metrics.py./v1/manage (HTMX, restricted
to internal/private network IPs) for adding and removing backend configs,
watching per-backend health, and viewing/resetting per-IP rate-limit usage.
The page connects to a WebSocket at /ws/configs, and the backend manager
broadcasts updated config and backend-health state to every connected admin
session whenever configs change or a health check alters the pool — no
manual refresh needed. When multiple configs share a name, the last one in
wins: the most recently created (or explicitly selected via the UI's
"select" button) config controls the backend pool for that name. The page
also shows live sparkline graphs per backend (healthy-server count,
retries, pool-exhaustion events) sampled directly from the in-process
Prometheus metrics — no external Prometheus/Grafana needed, see
Metrics dashboard.A request is processed by a stack of composable decorators, each able to short-circuit with an early response before it ever reaches a backend:
flowchart TD
A([Client request]) --> B[Hypercorn<br/>TLS termination]
B --> C["Quart @app.route<br/>URL match + metrics timer"]
C --> D{"@rate_limit<br/>per-IP limit?"}
D -->|exceeded| D1([429 Retry-After])
D -->|ok| E{"@modsec_waf<br/>WAF inspection"}
E -->|blocked| E1([403 / 400 / 414])
E -->|allowed| F[/"@apython_lb"/]
subgraph LB [apython_lb]
F --> G[URL rewrite<br/>url_match / url_rewrite]
G --> H{IP tracker<br/>penalised?}
H -->|yes| H1([429 Retry-After])
H -->|no| I{Healthy backend<br/>pool exists?}
I -->|no| I1([503 No backend pool])
I -->|yes| J["Select backend<br/>round_robin / sticky_ip / sticky_session"]
J --> K{Healthy backend<br/>available?}
K -->|no| K1([503 No healthy backends])
K -->|yes| L[DNS cache<br/>resolve host to IP]
L --> M[Forward via httpx]
M --> N{Response?}
N -->|success| O[Track 404s<br/>penalise over threshold]
N -->|"timeout / connect /<br/>request error"| P[Circuit breaker:<br/>drop backend from<br/>active list]
P --> Q{"Attempts left?<br/>< LB_MAX_ATTEMPTS"}
Q -->|yes| J
Q -->|no| Q1([502 / 504 All backends failed])
end
O --> R[after_request<br/>record metrics]
R --> S([Response to client])
| Variable | Default | Description |
|---|---|---|
IP_TRACKER_404_THRESHOLD | 4 | Number of 404s within the window before an IP is penalised |
IP_TRACKER_WINDOW_SECONDS | 120 | Rolling window (seconds) in which 404s are counted |
IP_TRACKER_PENALTY_DURATION | 300 | How long (seconds) a penalised IP is blocked |
RATE_LIMIT_REQUESTS | 100 | Max requests per IP within the rate limit window (app-wide limiter) |
RATE_LIMIT_WINDOW_SECONDS | 60 | Rolling window (seconds) for the rate limiter |
LOG_LEVEL | INFO | Effectively a two-position switch (see Logging): INFO shows only hypercorn access/startup logs; DEBUG shows all app logs too. Config lives in log_config.py, applied by both main.py and hypercorn_config.py |
APP_SECRET_KEY | (auto-generated) | Secret key used for sticky session signing. If unset, a random per-process key is generated at startup (logged as a WARNING) — sticky sessions then won't survive restarts or span replicas, so set it explicitly in production |
MANAGE_BASIC_AUTH | (unset) | Optional user:password for HTTP Basic auth on the management UI, /ws/configs, and /metrics, on top of the internal-IP restriction. Set it in any deployment where the internal network is not fully trusted |
MAX_CONTENT_LENGTH | 16777216 | Maximum request body size in bytes (bodies are buffered in memory for WAF inspection and forwarding) |
HEALTH_CHECK_INTERVAL | 60 | How often (seconds) backends are health-checked. A backend is healthy only when it answers GET / with HTTP 200 (see Health checks) |
A backend server is considered healthy only when GET / (with the
configured Host header) returns HTTP 200 — redirects, 204, or any
other status mark it unhealthy. This is a deliberate, fixed contract: point
the backend's root at something that returns 200, or terminate a
lightweight 200 responder on /. The hc_path config field is accepted and
stored but currently reserved — health checks always probe /. If your use
case needs a configurable path or status set, fork and adapt; if it becomes a
popular request we'll revisit with community input.
/health/health reports whether this container is actually ready to receive
traffic — the intended use is a Kubernetes readiness/liveness probe or a
Docker HEALTHCHECK, so a rolling deploy doesn't send requests to a
container whose backend pools haven't been verified healthy yet:
200. There's nothing to be unhealthy.200 only once they've been verified healthy by
the background health-check loop — being configured is not enough, a
freshly-started container returns 503 until its first health-check pass
confirms at least one healthy server per backend (or per the
HEALTH_READY_MIN_BACKENDS/HEALTH_READY_BACKENDS tunables below).The response body reports counts (never backend names/IPs, to avoid leaking topology on a route that's typically not internal-only):
{"status": "healthy", "backends_ready": 2, "backends_total": 2}
By default every configured backend pool must be healthy before /health
goes green. Two tunables loosen that for rollouts where waiting on every
backend isn't the right trade-off:
HEALTH_READY_MIN_BACKENDS=<n> — ready once at least n configured
backends are healthy, instead of all of them. 0 short-circuits
entirely: /health goes green as soon as any backend exists, without
waiting on health checks at all.HEALTH_READY_BACKENDS=<name>[,<name>...] — ready once every named
backend is healthy, ignoring the rest. Useful when one backend is a
reliable signal that the container is correctly configured and the others
are slower to warm up.The /v1/manage page renders live sparkline graphs per backend directly
from the metrics already defined in metrics.py — no external Prometheus
server or Grafana needed, and nothing leaves the process to produce them.
classes/metrics_history.py samples a curated set of metrics straight off
the in-process registry (.collect(), not even an internal HTTP call to
this app's own /metrics) on a fixed interval, keeping a short bounded
rolling window per backend in memory, and the management WebSocket pushes
updated sparklines to every connected admin session on that same cadence.
Three charts per backend, chosen because together they show a backend-pool incident developing and resolving in real time:
| Chart | Metric | Shape |
|---|---|---|
| Healthy servers | backend_healthy_servers{backend_name} | Raw value over time |
| Retries / interval | lb_retries_total{backend_name} | Delta since the previous sample, not the cumulative total — a timeout storm should look like a spike, not a permanent plateau |
| Pool-exhausted events / interval | lb_no_healthy_backends_total{backend_name} | Same delta treatment — this is the direct signal for "requests are 503ing because the pool emptied" |
History resets on restart (it's a live-debugging aid, not an audit log) and
is capped at METRICS_HISTORY_POINTS samples, taken every
METRICS_HISTORY_INTERVAL seconds (defaults: 180 points at 5s intervals,
~15 minutes of history). This is a view onto data that already exists for
anyone scraping /metrics with a real Prometheus — it doesn't replace that
for long-term retention, alerting, or cross-instance aggregation, it just
means you don't need one stood up to see what's happening on this instance
right now.
remote_addr — X-Forwarded-For is deliberately not trusted. Run
apython_lb as the first hop from clients. If you must front it with another
proxy, every client collapses into the proxy's IP: shared rate limits,
shared 404 penalties, and (for private proxy IPs) an internal-looking
source for the management UI — so set MANAGE_BASIC_AUTH./v1/manage, /ws/configs, and /metrics are internal-only (RFC1918 /
loopback source IPs). IP checks alone are weak on shared networks; add
MANAGE_BASIC_AUTH for real authentication.LOG_LEVEL behaves as a single two-position switch rather than a graduated
severity dial:
INFO (default): only hypercorn access and startup logs are emitted.
Hypercorn's own loggers are pinned at INFO in log_config.py so request
access logs and startup messages always appear exactly once.DEBUG: everything above, plus all application logs (routing,
health checks, rate limiting, WAF, DNS resolution, etc.).All application log lines are emitted at DEBUG on purpose. This keeps normal
(INFO) operation to a clean hypercorn access log and means turning on app
diagnostics is a single, deliberate flip to DEBUG — there are no routine app
logs at INFO/WARNING/ERROR to reason about.
The one deliberate exception: security-significant conditions are logged at
WARNING/ERROR so they are visible at the default level — a requested WAF
that failed to initialise (engine load failure, ERROR), a per-request WAF
transaction failure (WARNING), and a missing APP_SECRET_KEY replaced by an
auto-generated key (WARNING). These indicate degraded protection, not
routine operation, and should never be hidden behind DEBUG.
httpx logs one HTTP Request: ... line per call at INFO — health checks and
proxied requests would otherwise flood the access log. It is pinned to WARNING
and only surfaces its per-request lines when LOG_LEVEL=DEBUG, following the
same rule as the app loggers.
Lower-level libraries are noisier still and bury the app's own logs even when
debugging: httpcore (connection internals), hpack (per-header HTTP/2
encoding), and aiosqlite (every query and operation) all emit at DEBUG. They
are pinned to ERROR at all levels, so only genuine failures appear.
No sensitive data is logged at any level. Request headers and bodies are
never logged. Session IDs are auth-equivalent tokens, so sticky-session logs
emit a non-reversible sess-<8 hex> tag (sha256-derived) instead of the raw
session ID — enough to correlate a session across log lines without exposing
the token. Client IPs are retained as core load-balancer operational data
(and are already present in hypercorn's access logs).
The image is based on rackitio/pythont,
a free-threaded (--disable-gil) build of CPython compiled on
debian:trixie-slim — no official Python image ships a free-threaded
variant for any version yet. Splitting that build out means this project's
own Dockerfile no longer carries the compile recipe, and pythont can be
versioned/released independently; see that repo to bump the pinned Python
version. PYTHON_GIL=0 is set here so the container stays free-threaded
even though aioquic (Hypercorn's HTTP/3 dependency) hasn't declared
itself GIL-safe and would otherwise make CPython silently fall back to the
regular GIL the moment HTTP/3 starts. That override is safe specifically
because of how Hypercorn runs QUIC (single event-loop thread, no thread
pool in that path) — see the comment above ENV PYTHON_GIL=0 in the
Dockerfile for the full reasoning, and revisit it if Hypercorn's or
aioquic's internals change.
The container listens on 443/tcp (HTTP/1.1, HTTP/2) and 443/udp (HTTP/3
over QUIC), both using the same /app/cert.pem / /app/key.pem.
Two env vars, read by hypercorn_config.py (CLI flags don't expand env vars
in the Dockerfile's exec-form CMD, so this is where they're wired up):
| Variable | Default | Description |
|---|---|---|
HYPERCORN_WORKERS | 1 | Number of worker processes (multiprocessing, spawn) sharing the bound ports via SO_REUSEPORT. Each is a completely independent process — its own interpreter, its own in-memory IP-tracker/rate-limiter state, its own connection to /app/data/apython_lb.db |
HYPERCORN_WORKER_CLASS | asyncio | asyncio or uvloop. Not trio: aiosqlite calls asyncio.get_event_loop() internally, which doesn't exist under trio's runtime, so the app fails at startup regardless of GIL/free-threading — trio isn't installed and isn't offered as a choice |
The PYTHON_GIL=0 safety argument above was verified against both options:
Hypercorn's uvloop_worker reuses the exact same single-threaded asyncio
QUIC code path (just a different loop underneath), and uvloop itself
ships real cp314t/cp315t wheels and doesn't trigger CPython's
GIL-reenable at all (confirmed empirically on both). See the Dockerfile
comment for the full breakdown, including why trio was ruled out
independent of any of this.
HYPERCORN_WORKERS > 1 surfaced a real startup race, also fixed here:
every worker process opens /app/data/apython_lb.db and switches it into
WAL mode concurrently, and that transition takes a brief exclusive lock
that doesn't honor the connection's busy_timeout — a losing worker used
to crash outright on database is locked instead of just waiting.
classes/sqlite_db.py now retries that specific case with backoff.
Load tested with k6 against a live deployment, exercising
the full request path (WAF inspection, rate limiting, IP tracking, backend
selection, and a real proxied response) rather than just /health. The
script is at scripts/k6.js — update the BASE_URL and
HOST_HEADER constants at the top (or pass them as -e flags) to point it
at your own deployment before running it.
1,500 virtual users, ramped up over 30s, held for 1m, ramped down over 30s:
| Metric | Result |
|---|---|
| Peak throughput | 925 req/s |
| p95 latency | 1.03s (threshold was <2s) |
| Failed requests | 0.00% (threshold was <1%) |
| Completed iterations | 56,775, zero interrupted |
| Checks passed | 113,550 / 113,550 (100%) |
k6 run -e BASE_URL=https://your-host -e HOST_HEADER=your.host.header scripts/k6.js
Hypercorn serves TLS and expects /app/cert.pem and /app/key.pem inside the container. *.pem files are gitignored, so a fresh clone has none — without them every worker crashes at startup with FileNotFoundError from create_ssl_context. Generate a self-signed pair and mount it:
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem \
-days 365 -nodes -subj "/CN=localhost"
docker build --target final -t apython_lb:local .
docker run --rm -p 8443:443/tcp -p 8443:443/udp \
-v "$PWD/certs/cert.pem":/app/cert.pem:ro \
-v "$PWD/certs/key.pem":/app/key.pem:ro \
apython_lb:local
curl -sk https://localhost:8443/health
The manifests in docs/k8s/deploy/ mount a apython-lb-tls secret to /app/cert.pem and /app/key.pem. Both the apython-lb and apython-lb-test deployments share this one secret, so create it once per cluster from your ACME-issued cert/key before applying the manifests. The deployments also read APP_SECRET_KEY from an app-secrets secret (see Variable Reference):
kubectl create secret tls apython-lb-tls \
--cert=./fullchain.pem \
--key=./privkey.pem \
--namespace=default
kubectl create secret generic app-secrets \
--from-literal=secret-key=<your-secret-key>
kubectl apply -f docs/k8s/deploy/dev-nodeport-apython-lb.yaml
kubectl apply -f docs/k8s/deploy/dev-nodeport-apython-lb-test.yaml
To rotate the TLS secret after renewing the cert:
kubectl create secret tls apython-lb-tls \
--cert=./fullchain.pem \
--key=./privkey.pem \
--namespace=default \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/apython-lb deployment/apython-lb-test --namespace=default
# Use the app-wide limiter (RATE_LIMIT_REQUESTS / RATE_LIMIT_WINDOW_SECONDS)
@app.route("/<path:path>", methods=["GET", "POST"])
@rate_limit
@apython_lb(backend_name="my_backend", route_policy="round_robin")
def proxy(path): pass
# Give a route its own dedicated limiter
@app.route("/api/<path:path>", methods=["GET", "POST"])
@rate_limit(limit=10, window=60)
@apython_lb(backend_name="my_backend", route_policy="round_robin")
def api_proxy(path): pass
Rejected requests get a 429 with a Retry-After header.
# Strip a /v1 prefix before forwarding
@app.route("/v1/", defaults={"path": ""})
@app.route("/v1/<path:path>", methods=["GET", "POST"])
@apython_lb(
backend_name="my_backend",
route_policy="sticky_ip",
url_match=r"^v1/(.+)$",
url_rewrite=r"\1",
)
def v1_proxy(path): pass
# Rewrite /api/users/123 → /users/123 using a named group
@app.route("/api/<path:path>", methods=["GET", "POST"])
@apython_lb(
backend_name="my_backend",
route_policy="round_robin",
url_match=r"^api/(?P<rest>.+)$",
url_rewrite=r"\g<rest>",
)
def api_proxy(path): pass
# Inject a fixed prefix: /files/foo.txt → /static/files/foo.txt
@app.route("/files/<path:path>", methods=["GET", "POST"])
@apython_lb(
backend_name="my_backend",
route_policy="round_robin",
url_match=r"^(files/.+)$",
url_rewrite=r"static/\1",
)
def files_proxy(path): pass
The examples assume the container from Running Locally,
listening on https://localhost:8443 (-k because the local cert is
self-signed). Backend IPs are comma-separated with no spaces; all five fields
are required (hc_path is stored but not yet used — see
Health checks):
curl -sk -X POST https://localhost:8443/v1/manage/configs \
-H "Content-Type: application/json" \
-d '{
"name": "multi-backend",
"proto": "https",
"host": "example.com",
"ips": "1.2.3.4,5.6.7.8,app.example.com",
"hc_path": "/healthz"
}'
Check it was saved:
curl -sk https://localhost:8443/v1/manage | grep -o 'config id: [0-9]*'
Delete by ID:
curl -sk -X DELETE https://localhost:8443/v1/manage/configs/1
The WAF is powered by libmodsecurity3 and is enabled by default in the Docker image and the bundled Kubernetes manifests. The engine is implemented as a ctypes wrapper around the libmodsecurity C API — no compiled Python extension or build tools are required. The rules file shipped at /app/modsecurity.conf is loaded automatically on startup.
| Variable | Docker default | Description |
|---|---|---|
MODSECURITY_ENABLED | true | Set to false to disable the WAF |
MODSECURITY_RULES_FILE | /app/modsecurity.conf | Path to the ModSecurity rules file |
MODSECURITY_FAIL_CLOSED | false | When true, refuse to start (and reject requests on per-request engine failures with a 500) if the WAF was requested but cannot run. When false, such failures log at ERROR/WARNING and traffic continues uninspected. The waf_engine_enabled Prometheus gauge reports the live state |
| ID | Status | Description |
|---|---|---|
910001 | 400 | Null byte (\x00) in URI or request headers |
910002 | 400 | Path traversal pattern (../, ..\) in URI or query args |
910003 | 400 | Ambiguous Transfer-Encoding value (request smuggling vector) |
910004 | 414 | Request URI longer than 2048 characters |
910010 | 403 | User-Agent matches a known security scanner (sqlmap, nikto, nmap, masscan, w3af, skipfish, dirbuster, gobuster, wfuzz, Burp Suite, Metasploit, Hydra, Havij, Acunetix, Nessus, OpenVAS) |
910020 | 403 | SQL injection detected in URI or query string (via libinjection @detectSQLi) |
910021 | 403 | XSS detected in URI or query string (via libinjection @detectXSS) |
| ID | Status | Description |
|---|---|---|
910030 | 403 | SQL injection detected in request body or parsed args (via libinjection @detectSQLi) |
910031 | 403 | XSS detected in request body or parsed args (via libinjection @detectXSS) |
910032 | 403 | Remote file inclusion — http://, https://, or ftp:// URL found in request body or args |
Note on rule
910032: it blocks any request body containing a URL, which is a large false-positive surface for JSON APIs whose payloads legitimately carry links. If your backends accept URLs in request bodies, remove or tune this rule inmodsecurity.conf.
@app.route("/<path:path>", methods=["GET", "POST"])
@rate_limit
@modsec_waf
@apython_lb(backend_name="my_backend", route_policy="round_robin")
def proxy(path): pass
@modsec_waf is a no-op when app.modsec is not configured, so it is safe to leave on routes even when the WAF is disabled.
A script at tests/smoke_waf.sh exercises all testable rules against a running container. No backends are needed — the WAF blocks requests before they reach the load balancer.
# Start the container
docker run --rm -p 8443:443 \
-e MODSECURITY_ENABLED=true \
-v "$PWD/certs/cert.pem":/app/cert.pem:ro \
-v "$PWD/certs/key.pem":/app/key.pem:ro \
apython_lb:local
# In a second terminal (defaults to https://localhost:8443)
bash tests/smoke_waf.sh
# Or against a remote host
bash tests/smoke_waf.sh https://your-host:443
The script exits non-zero if any check fails, making it CI-safe.
Rule 910003 (Transfer-Encoding smuggling) is not covered by the smoke test. Hypercorn processes the Transfer-Encoding header at the transport layer before the ASGI app sees the request, so it never reaches the application-layer WAF. The rule is still worth keeping for deployments where a reverse proxy or different server forwards the raw headers through.
Path traversal (rule 910002) is tested via a query parameter (?file=../../etc/passwd) rather than the URL path because curl normalises /../ segments before sending.
The project uses ruff for linting and formatting, configured in ruff.toml.
python3 -m pip install --user -r requirements-dev.txt # installs ruff
ruff check . # lint
ruff check --fix . # lint + autofix
ruff format . # format
A pre-commit hook in .githooks/ runs ruff check --fix and ruff format on staged
Python files, re-stages the results, and blocks the commit only if unfixable lint
errors remain. Enable it after cloning:
git config core.hooksPath .githooks
That's all teammates need — the hook is tracked in the repo, so it stays in sync.
Requires ruff on your PATH or importable via python3 -m ruff (installed by
requirements-dev.txt).
The project uses pytest and includes a dedicated Docker test stage. The production image does not include the tests/ directory.
Build and run only the test stage from Docker:
docker build --target test -t apython_lb-test .
If the tests fail, the build will fail immediately.
If you want to run tests locally instead of in Docker:
python3 -m pip install --user -r requirements-dev.txt
python3 -m pytest -q tests
Releases are cut by pushing a semver tag. The release GitHub Actions
workflow (.github/workflows/release.yml)
then:
test stage (pytest) — a failure stops the release before
anything is published;final stage for linux/amd64 and linux/arm64;<DOCKERHUB_USERNAME>/apython_lb tagged
X.Y.Z, X.Y, and latest.One-time setup: add two repository secrets under Settings → Secrets and
variables → Actions — DOCKERHUB_USERNAME (also used as the image
namespace) and DOCKERHUB_TOKEN (a Docker Hub access
token with Read & Write scope).
To cut a release:
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0
Then pull the published image:
docker pull <dockerhub-username>/apython_lb:0.1.0
pytest before merge.test stage is the canonical verification path and should be used in CI if possible.Test coverage is the best way to keep the project reliable as it grows.
CONFIG_POLL_INTERVAL | 10 | How often (seconds) the DB is polled for config changes |
METRICS_HISTORY_INTERVAL | 5 | How often (seconds) the management UI's metrics sparklines are sampled — see Metrics dashboard |
METRICS_HISTORY_POINTS | 180 | How many samples of history each metrics sparkline keeps (default: ~15 minutes at the 5s interval default) |
DNS_NAMESERVERS | 8.8.8.8,1.1.1.1 | Comma-separated DNS nameservers for backend hostname resolution (the Docker image sets 1.1.1.1) |
LB_MAX_ATTEMPTS | 3 | Total tries per request before giving up (initial attempt + retries on timeout/connect/request errors) |
LB_UPSTREAM_HTTP2 | true | Whether the upstream httpx client negotiates HTTP/2 |
LB_UPSTREAM_VERIFY_TLS | true | Whether the upstream httpx client (proxying and health checks) verifies backend TLS certificates. Only disable for backends with self-signed certs you cannot fix |
LB_UPSTREAM_TIMEOUT_SECONDS | 30 | Per-request timeout (seconds) for forwarding to a backend |
STICKY_SESSION_MAX_ENTRIES | 10000 | Maximum tracked session→backend pins; oldest pins are evicted at capacity |
STICKY_IP_MAX_ENTRIES | 10000 | Maximum tracked IP→backend pins; oldest pins are evicted at capacity |
HEALTH_READY_MIN_BACKENDS | (unset = all) | Minimum number of configured backend pools that must each have at least one verified-healthy server before /health returns 200. Unset requires all of them; 0 makes /health ready as soon as any backend is configured, regardless of health state — a short circuit for fast rollouts. Clamped to the configured backend count, so an over-large value can't make /health permanently unready. Ignored if HEALTH_READY_BACKENDS is set (see Health checks) |
HEALTH_READY_BACKENDS | (unset) | Comma-separated backend names that, if set, are the only ones /health waits on — point it at one canary backend during a rollout and let the rest catch up asynchronously. Overrides HEALTH_READY_MIN_BACKENDS |