⚡ High-performance reverse proxy with Auto-SSL, HTTP/2, and 6 LB algorithms. Built with Rust.
10K+
Zunik is a lightweight reverse proxy & load balancer written in Rust, configured with plain TOML, with built-in Let's Encrypt and hot-reload.
Features
http:// and https:// backends with connection poolinground_robin, weighted_round_robin, random, ip_hash, least_connections, least_response_timeRange/206, 304 Not Modified) and WebSocket proxyingtrusted_proxies: only trusted upstreams (Cloudflare, load balancers) can set X-Forwarded-For / X-Forwarded-Proto/metrics with per-route latency histograms and per-backend statsSIGHUP, lock-free config swapSIGTERM/SIGINT (drains in-flight requests)include or --config-dir) and zunik -t config testCreate config/zunik.toml:
[server]
http_listen = "0.0.0.0:80"
[dashboard]
listen = "0.0.0.0:8080"
[routes.app.match_rule]
path_prefix = "/"
[[routes.app.backends]]
url = "http://your-backend:3000"
Run it:
docker run -d --name zunik \
-p 80:80 -p 8080:8080 \
-v $(pwd)/config/:/app/config/:ro \
zunohan/zunik:latest
Open the dashboard at http://localhost:8080.
A single file works too: -v $(pwd)/config.toml:/app/config.toml:ro. When /app/config.toml is absent, Zunik loads every .toml file in /app/config/.
Validate a config before deploying:
docker run --rm -v $(pwd)/config/:/app/config/:ro zunohan/zunik:latest -t
services:
zunik:
image: zunohan/zunik:latest
ports:
- 80:80
- 443:443
- 127.0.0.1:8080:8080 # keep the dashboard private
volumes:
- ./config/:/app/config/:ro
- zunik-certs:/app/certs
ulimits:
nofile:
soft: 65536
hard: 65536
stop_grace_period: 35s # > graceful_shutdown_secs (default 30)
restart: unless-stopped
backend:
image: your-app:latest
expose:
- 3000
volumes:
zunik-certs:
[server]
http_listen = "0.0.0.0:80"
https_listen = "0.0.0.0:443"
[server.auto_ssl]
domains = ["example.com", "www.example.com"]
email = "[email protected]"
cert_dir = "/app/certs"
staging = false
renew_before_days = 30
<cert_dir>/<first-domain>.cert.pem / .key.pem. Persist /app/certs in a volume.staging = true to false, or when adding domains.Manual certificates (key must be PKCS#8):
[server]
https_listen = "0.0.0.0:443"
[server.tls]
cert_file = "/app/certs/fullchain.pem"
key_file = "/app/certs/privkey.pem"
[routes.api]
load_balancing = "least_connections"
priority = 10
timeout_secs = 15
max_body_size = 10485760
[routes.api.match_rule]
host = "api.example.com" # or "*.example.com"
path_prefix = "/v1"
methods = ["GET", "POST"]
[[routes.api.backends]]
url = "http://api-1:3000"
weight = 3
[[routes.api.backends]]
url = "http://api-2:3000"
weight = 1
[routes.api.health_check]
path = "/health"
interval_secs = 10
unhealthy_threshold = 3
healthy_threshold = 2
[routes.api.retry]
attempts = 3
delay_ms = 200
retry_on_statuses = [502, 503, 504]
Higher priority wins. With equal priority, the most specific match wins (exact path > host > regex > longest prefix > method > headers).
[[routes.api.middlewares]]
type = "cors"
allowed_origins = ["https://app.example.com"]
[[routes.api.middlewares]]
type = "rate_limit"
requests_per_second = 100
burst = 20
[[routes.api.middlewares]]
type = "strip_prefix"
prefix = "/v1"
[[routes.api.middlewares]]
type = "compress"
algorithms = ["br", "gzip"]
[[routes.admin.middlewares]]
type = "basic_auth"
[routes.admin.middlewares.users]
admin = "change-me"
Available: cors, add_headers (response headers), basic_auth, rate_limit, strip_prefix, add_prefix, rewrite_path, compress, redirect_https.
redirect_https only redirects plain HTTP requests. HTTPS requests and requests with X-Forwarded-Proto: https (TLS terminated upstream) pass through, so it is safe on routes served over both.
[routes.assets.match_rule]
path_prefix = "/assets"
[routes.assets.static_files]
root = "/app/public"
index = "index.html"
directory_listing = false
cache_secs = 3600
config/
├── 00-server.toml # [server], [dashboard]
├── 10-users.toml # [routes.users]
└── 20-orders.toml # [routes.orders]
Files load in alphabetical order. Routes are merged (later files win on name clashes). Editing any .toml file in the directory reloads routes automatically, or force a reload with docker kill -s HUP zunik. Listener, TLS and dashboard settings require a restart.
Config lookup order: --config-dir → --config file (if it exists) → ./config/ (if it has .toml files) → ./config.toml. An existing but invalid config makes Zunik exit with code 1 instead of starting with no routes.
By default Zunik trusts nobody's forwarding headers: the client IP is the TCP peer. Declare your upstream proxies so the real client IP and scheme are used for ip_hash, rate_limit, X-Real-IP and redirect_https:
[server]
trusted_proxies = ["173.245.48.0/20", "10.0.0.0/8"] # IPs or CIDRs
[server]
max_connections = 10000
header_read_timeout_secs = 10 # close clients that don't send headers in time
tls_handshake_timeout_secs = 10
http2_max_concurrent_streams = 250
| Base | debian:bookworm-slim, runs as non-root user zunik |
| Working dir | /app |
| Entrypoint | /app/zunik --config /app/config.toml |
| Volumes | /app/config (config), /app/certs (certificates) |
| Ports | 80 HTTP, 443 HTTPS, 8080 dashboard |
| Healthcheck | curl -sf http://localhost:8080/api/health (requires the dashboard on port 8080) |
| Endpoint | Description |
|---|---|
GET / | Web dashboard |
GET /api/metrics | Requests, connections, bytes, RPS, error rate, status codes, route hits, per-backend stats |
GET /api/config | Running configuration (JSON) |
GET /api/health | Liveness status (always public) |
GET /metrics | Prometheus metrics: zunik_requests_total, zunik_responses_total{code}, zunik_request_duration_seconds, zunik_route_request_duration_seconds{route}, zunik_backend_up{backend}, ... |
Protect it with Basic Auth:
[dashboard]
listen = "0.0.0.0:8080"
username = "admin"
password = "a-long-random-password"
Secrets in /api/config (dashboard and basic_auth passwords) are always redacted. For access over the internet, prefer binding to 127.0.0.1 and using an SSH tunnel.
| Variable | Default | Description |
|---|---|---|
RUST_LOG | zunik=info | Log level, e.g. zunik=debug, zunik=warn |
Full documentation and source code: https://github.com/zuno90/zunik
MIT
Content type
Image
Digest
sha256:f54b945b8…
Size
37.5 MB
Last updated
8 days ago
docker pull zunohan/zunik