TLS termination with SNI multi-domain routing, active health checks,
an LRU response cache, per-IP rate limiting, WebSocket tunnelling and a
Prometheus /metrics endpoint — from one TOML file. This
is the proxy in front of every domain here.
Define upstreams, TLS certs, and limits in a single TOML file.
# proxy.toml [server] listen = 443 workers = 64 tls_cert = "/etc/letsencrypt/live/example.com/fullchain.pem" tls_key = "/etc/letsencrypt/live/example.com/privkey.pem" rate_limit = 100 access_log = "/var/log/nyx-proxy/access.log" [upstream.0] name = "web" host = "127.0.0.1" port = 3000 hostname = "example.com" [upstream.1] name = "api" host = "127.0.0.1" port = 3001 hostname = "api.example.com" # every <customer>.example.com not listed above [upstream.2] name = "tenants" host = "127.0.0.1" port = 3002 hostname = "*.example.com"
Six steps take a fresh Linux box to a TLS-terminating proxy running under systemd on port 443. This is the exact shape that fronts every domain here.
nyx-proxy is a Nyx library. Install the compiler, clone, build the single binary.
$ curl -sSf https://nyxlang.com/install.sh | sh $ sudo apt install -y clang certbot # build + TLS deps $ git clone https://github.com/nyxlang-dev/nyx-proxy $ cd nyx-proxy && nyx build # produces ./nyx-proxy
Create A records for each domain you'll serve, pointing at the server's public IP. Confirm they resolve before issuing certs.
$ dig +short example.com # must return your server IP $ dig +short api.example.com
Let's Encrypt via certbot's standalone mode (it binds :80 briefly — make sure nothing else holds it yet). One cert per domain, or one multi-domain cert.
$ sudo certbot certonly --standalone \ -d example.com -d api.example.com # certs land in /etc/letsencrypt/live/example.com/
Point tls_cert / tls_key at the issued cert, list your upstreams by hostname (see the config above), and turn on caching, metrics and rate limiting as needed.
[server] listen = 443 workers = 64 tls_cert = "/etc/letsencrypt/live/example.com/fullchain.pem" tls_key = "/etc/letsencrypt/live/example.com/privkey.pem" [cache] enabled = 1 [metrics] enabled = 1 port = 9090
A unit keeps it alive across reboots and crashes. It needs to bind :443, so it runs as root (or with CAP_NET_BIND_SERVICE).
# /etc/systemd/system/nyx-proxy.service
[Unit]
Description=nyx-proxy
After=network-online.target
[Service]
ExecStart=/srv/nyx-proxy/nyx-proxy /srv/nyx-proxy/proxy.toml
WorkingDirectory=/srv/nyx-proxy
Restart=always
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
$ sudo systemctl enable --now nyx-proxy $ systemctl status nyx-proxy # active (running)
Hit each domain over HTTPS, confirm a cache HIT, and scrape metrics. Certbot's timer renews certs automatically — reload the proxy after a renewal.
$ curl -sI https://example.com | head -1 # HTTP/2 200 $ curl -sD - https://example.com -o /dev/null | grep X-Nyx-Cache X-Nyx-Cache: HIT $ curl -s http://127.0.0.1:9090/metrics | head
certbot installs a systemd timer that renews before expiry. Add a deploy hook — --deploy-hook "systemctl reload nyx-proxy" — so the proxy picks up the new cert without a manual restart.
hostname = "*.example.com" sends every one-label subdomain to one upstream — new tenants go live without touching the config. Exact hostnames always win over the wildcard, whatever their order; like a wildcard certificate, it covers neither the bare domain nor a.b.example.com.X-Nyx-Cache: HIT.2026-04-02 10:15:23 GET / 200 12ms 2026-04-02 10:15:24 GET /api/users 200 3ms 2026-04-02 10:15:24 POST /api/data 201 8ms 2026-04-02 10:15:25 GET /assets/style.css 200 1ms 2026-04-02 10:15:30 GET /api/spam 429 0ms
For simple reverse proxy setups, nyx-proxy does the same job in a fraction of the size.