Full documentation: https://cli.trillium.rs.
A single trillium binary that bundles the most useful pieces of the
trillium.rs web stack into a batteries-included HTTP
toolkit:
serve— a static file server (and drop-in reverse proxy)proxy— a reverse/forward proxy with upstream load-balancing and cachinggateway— a config-driven server combining static files + proxy across one or more listenersclient— a curl-like HTTP client that pretty-prints JSON and follows redirectsbench— a load generator with HDR-histogram latency statisticsdev-server— watch/rebuild/restart loop with browser live-reload for trillium apps (opt-in feature, Unix only)
TLS is built in (rustls by default), and with the default h3 feature the
servers also speak HTTP/3 over QUIC. Over TLS the client negotiates HTTP/2 via
ALPN; --http-version selects the protocol (HTTP/1.0 through HTTP/3) for
client and bench.
The binary is named trillium. Run trillium --help, or
trillium <command> --help, for the full option list — the examples below
cover the common cases.
Each release ships prebuilt binaries for x86_64/aarch64 macOS, x86_64 Linux,
and x86_64 Windows — no Rust toolchain, no compile. They're built with every
subcommand enabled (gateway, grpc, and, on macOS/Linux, dev-server), so
you get the full toolkit out of the box.
If you have cargo-binstall, it reads the release metadata and fetches the right binary for your platform:
cargo binstall trillium-cliOtherwise, the installer scripts detect your platform, download the matching
archive, and place the binary in ~/.cargo/bin:
macOS and Linux:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/trillium-rs/trillium-cli/releases/latest/download/trillium-cli-installer.sh | shWindows (PowerShell):
powershell -c "irm https://github.com/trillium-rs/trillium-cli/releases/latest/download/trillium-cli-installer.ps1 | iex"Both always install the most recent release. You can also pin a specific version or download the archives directly from the releases page.
To compile from source with cargo:
cargo install trillium-cliA default cargo install builds only serve, proxy, client, and bench
(the prebuilt binaries above bundle the rest). Select
features to add the others:
cargo install trillium-cli --features gateway,grpc,dev-serverMost listening options also read from environment variables (HOST, PORT,
CERT, KEY, FORWARD, UPSTREAM), so they compose well with .env files
and process managers.
Serve the current directory on http://localhost:8080:
trillium servePick a directory and port, and serve over your LAN:
trillium serve ./public --host 0.0.0.0 --port 3000Responses are compressed (gzip/brotli/zstd) automatically based on the client's
Accept-Encoding; pass --no-compress to turn that off.
Directory listings. By default a request for a directory with no index file
returns 404 Not Found. Pass -l / --directory-listing (or set
DIRECTORY_LISTING=1) to instead render an HTML index of the directory's
contents, with clickable column headers that sort by name, size, or modification
time:
trillium serve ./files --directory-listingIt's off by default because it exposes file names and structure. Configuring an
--index file takes precedence — listings only appear for directories without
one.
Single-page apps & reverse proxying. --forward turns any request that
would 404 into a reverse proxy to another origin — perfect for serving a built
frontend while passing /api calls through to a backend:
trillium serve ./dist --forward http://localhost:4000Rate limiting. Cap requests per client network. Over-quota requests get
429 Too Many Requests with a Retry-After header, and every metered response
advertises the standard RateLimit / RateLimit-Policy headers:
trillium serve --rate-limit 100/min # sustained 100 req/min per network
trillium serve --rate-limit 10/s --rate-limit-burst 50 # allow short spikesRates are written COUNT/WINDOW, where the window is s, min, or h.
Proxy all traffic to a single upstream:
trillium proxy http://localhost:4000Load-balance across several upstreams (default strategy is round-robin):
trillium proxy http://app-1:4000 http://app-2:4000 --strategy connection-countingStrategies: round-robin, connection-counting, random, and forward (a
classic forward proxy, including CONNECT tunneling — pass no upstreams).
The proxy ships with a response cache (honoring caching headers),
compression, WebSocket upgrade passthrough, and the same --rate-limit
controls as serve:
# 1 GiB in-memory cache, evict entries idle for 5 minutes, throttle abusive clients
trillium proxy http://localhost:4000 \
--cache-memory-capacity 1GiB --cache-time-to-idle 5m \
--rate-limit 1000/minThe cache is in-memory by default; point --cache-disk at a directory to add a
durable on-disk tier that survives restarts (--cache-disk alone tiers hot
memory over disk; add --cache-memory-capacity 0 to cache only to disk). Use
--no-cache to disable caching entirely. When an upstream is https://,
select a client TLS backend with --client-tls (-k/--insecure skips
verification for self-signed dev certs).
Where serve and proxy each do one thing from flags, gateway reads a
KDL config file and assembles the same building blocks —
static files, reverse proxy, redirects, header & HTML rewriting, compression,
rate limiting, TLS/h3 — into one or more listeners. It's a trillium-backed
caddy/nginx-lite.
trillium gateway --config gateway.kdl
trillium gateway --config gateway.kdl --check # parse + print the resolved config, don't serveA binding is one listener (host:port + optional TLS + per-binding HTTP
tuning). Within it, ordered route patterns dispatch by path to a stack of
directives:
compression true
rate-limit "100/min" burst=200
// Opt-in response cache for proxied upstreams (off unless declared, unlike
// `trillium proxy`). A bare `cache` node enables it with defaults. Declare
// `memory`, `disk`, or both: `disk` (and the tiered `memory`+`disk` form)
// persists cached responses across restarts.
cache {
memory "256MiB"
disk "/var/cache/trillium" size="10GiB"
time-to-idle "5m"
}
binding ":443" {
tls cert="./cert.pem" key="./key.pem"
http {
received-body-max-len "10MiB"
}
route "/api/*" {
// /api is stripped (the route pattern controls stripping, like `files`);
// give the upstream a base path to forward *with* the prefix instead.
proxy strategy="round-robin" {
upstream "http://127.0.0.1:9000"
upstream "http://127.0.0.1:9001"
}
}
route "/old/*" {
redirect "https://example.com/new" status=308
}
route "/*" {
headers {
add "X-Served-By" "trillium"
remove "Server"
}
files root="./public" index="index.html" directory-listing=true
}
}Declare multiple binding blocks to run several listeners in one process; a
single Ctrl-C drains all of them gracefully. A bare :443 host binds all
interfaces (the nginx listen :80 convention). Routes match by path
specificity, for all HTTP methods.
HTML rewriting. A rewrite-html directive streams the response body through
lol-html, applying ordered mutations to the elements
matched by CSS selectors — inject tags, rewrite attributes, or strip nodes from
a static page or a proxied upstream. Only text/html responses are touched;
JSON and binary stream through untouched, so it's safe to drop in front of a
mixed proxy. CSS selectors are validated when the config loads (with a source
span pointing at any unsupported selector), not on the first request.
route "/*" {
proxy {
upstream "http://127.0.0.1:9000"
}
rewrite-html {
select "head" {
append "<script src=\"/analytics.js\" async></script>"
}
select "a[target=_blank]" {
set-attribute "rel" "noopener noreferrer"
}
select "img" {
set-attribute "loading" "lazy"
}
select ".legacy-banner" {
remove
}
select "title" {
set-text "Proxied by trillium"
}
}
}Each select "css-selector" block holds an ordered list of element mutations.
Markup-valued ops (before, after, prepend, append, set-inner,
replace) insert their argument as HTML; set-text inserts HTML-escaped text.
The rest: set-attribute "name" "value", remove-attribute "name",
set-tag "div", remove (delete the element and its content), and unwrap
(drop the element's tags but keep its content).
Virtual hosting. Put host blocks inside a binding to dispatch by Host
header on a shared socket. Patterns are exact (example.com), wildcard
(*.example.com, any subdomain), or * (any). A request matching no host
block falls back to the binding's direct routes — which also catches requests
with no Host header (HTTP/1.0):
binding ":443" {
tls cert="./cert.pem" key="./key.pem"
host "app.example.com" {
route "/*" {
proxy {
upstream "http://127.0.0.1:9000"
}
}
}
host "*.static.example.com" {
route "/*" {
files root="./public"
}
}
// default vhost: unmatched hosts (and Host-less requests)
route "/*" {
redirect "https://example.com" status=308
}
}A curl-like client that pretty-prints JSON, streams bodies, and follows redirects by default:
trillium client get https://example.com
trillium client get https://httpbin.org/jsonSend headers and a body (from the command line, a file, or stdin):
trillium client post https://httpbin.org/anything \
-H Authorization="Bearer $TOKEN" Content-Type=application/json \
-b '{"hello": "world"}'
trillium client post https://httpbin.org/anything -f ./body.json
cat ./body.json | trillium client post https://httpbin.org/anythingStream a Server-Sent Events endpoint with --sse. At a terminal each event is
rendered with colored field labels (event type, id, retry) and JSON payloads
are pretty-printed; piped, events become newline-delimited JSON for jq:
trillium client get https://example.com/events --sse
trillium client get https://example.com/events --sse | jq .dataOther handy flags: --output-file to save the body, --dry-run to print the
request without sending it, -c/--compression to compress the request body
(zstd/br/gzip), --retry to retry failed requests with backoff,
--timeout/--no-timeout, and --no-follow-redirects / --max-redirects to
control redirect behavior.
Closed-loop: 50 concurrent connections for 10 seconds (defaults):
trillium bench https://localhost:8080Open-loop at a target arrival rate (switches to scheduled load, useful for measuring latency under a fixed offered rate):
trillium bench https://localhost:8080 --rate 5000 --pacing poisson --duration 30sResults are reported as an HDR-histogram latency summary. Add --json for a
machine-readable report on stdout, or --csv <path> for per-request timing
data. --connections, --requests, --warmup, and --timeout round out the
common knobs.
A watch / rebuild / restart loop with browser live-reload. It's Unix-only. The
prebuilt macOS/Linux binaries already include
it; with cargo install it's feature-gated, so enable it explicitly:
cargo install trillium-cli --features dev-serverRun it from your app's project root and open the address it listens on:
trillium dev-server # then visit http://localhost:8080It watches your source, rebuilds with cargo on change, restarts your binary,
and serves a reload-injecting proxy in front of it. The dev server adopts your
HOST/PORT (so you visit the same address you'd use in production) and runs
your app on a private port behind the proxy, passing it through as PORT. Use
--app-port if your app hardcodes its own port instead of reading PORT.
In a workspace it watches the crate it builds plus the workspace-local crates
it depends on, so editing a path-dependency library reloads the app using it.
Select what to build with cargo's own flags via --build-args, and pass any
runtime arguments your binary needs (a subcommand, a flag) with --run-args:
trillium dev-server --build-args "-p my-app --features dev"
trillium dev-server --example hello-world
trillium dev-server --build-args "-p my-app" --run-args serveWhen a build fails, the errors render as an overlay in the browser (the previous
build keeps running underneath). Click a file:line:column and it opens in your
editor — $EDITOR by default, or --editor "code --wait" — jumping to the
line. It also applies safe dev-build speedups (trimmed debug info + a fast linker
if one's installed); disable them with --no-fast.
See the dev-server guide for the full
details.
Provide a certificate and key to serve over TLS (and, with the default h3
feature, HTTP/3 over QUIC on the same port):
trillium serve --cert ./cert.pem --key ./key.pem
# or via the environment:
CERT=./cert.pem KEY=./key.pem trillium serveFor local development, mkcert or
rcgen will generate a trusted cert/key pair. Test an HTTPS+h3 server with
curl -k https://localhost:8080.
git clone https://github.com/trillium-rs/trillium-cli
cd trillium-cli
cargo build --release # release builds use fat LTOEach subcommand is gated behind a Cargo feature, so you can build a smaller binary with only what you need:
| Feature | Subcommand | Default | Notes |
|---|---|---|---|
serve |
serve |
✅ | static file server + reverse proxy |
proxy |
proxy |
✅ | reverse/forward proxy with caching |
gateway |
gateway |
config-driven multi-listener server (KDL) | |
client |
client |
✅ | HTTP client |
bench |
bench |
✅ | load generator |
dev-server |
dev-server |
watch/rebuild/restart loop (Unix only) | |
grpc |
grpc |
generate Rust modules from .proto files |
TLS backends are selectable too: rustls (default), native-tls, and
openssl. The h3 feature (default) adds HTTP/3 over QUIC and implies
rustls.
# just the client, built against the system's native TLS
cargo install trillium-cli --no-default-features --features client,native-tlsLicensed under either of MIT or Apache-2.0 at your option.