A simple static web server written in Rust. Run www to serve the current directory over HTTP on port 8000, with HTTPS available when you need it.
To make starting a web server as simple as running one command. The project began with the need to share files and test the contents of a directory over HTTP, without setting up a web framework or writing a configuration file.
If you usually reach for:
python3 -m http.server 8000Use www as a standalone alternative:
wwwOpen http://localhost:8000/ to browse the current directory. Serve a different directory with -r, choose a port with -p, or add HTTPS with -s localhost for local testing and -s DOMAIN for automatic Let's Encrypt certificates.
If you have Rust 1.88+ and Cargo (Rust's package manager) installed:
cargo install www --lockedCargo installs the executable into ~/.cargo/bin by default. Make sure that directory is in your PATH so your shell can find www.
Download an archive or package for your operating system and CPU architecture from GitHub Releases. Prebuilt binaries do not require Rust or Python to run.
For a .tar.gz archive, extract it and run ./www from the extracted directory, or place the www executable in a directory on your PATH to run it from anywhere. DEB and RPM packages install the command through your system's package manager.
Available builds cover Linux (x86_64, i686, and aarch64 GNU archives; static musl archives and DEB/RPM packages on x86_64 and aarch64), FreeBSD (x86_64, i686, aarch64), and macOS (x86_64, aarch64).
Requires Git, Rust 1.88+, and the just command runner:
git clone https://github.com/nbari/www.git
cd www
just build
just install "$HOME/.local"Make sure ~/.local/bin is in your PATH.
In a terminal, change to the directory you want to serve and run:
wwwOpen http://localhost:8000/ in your browser. If the directory contains index.html, you will see that page; otherwise, you will see a list of files. Keep the terminal running while you browse, and press Ctrl+C to stop the server.
www serves static files, such as HTML, CSS, JavaScript, images, and downloads. It does not execute server-side code such as PHP or Python.
By default, the server also accepts connections from other machines. On the same network, open http://<server-ip>:8000/, replacing <server-ip> with the serving machine's network IP address; your firewall must allow the connection. Share only directories intended for others to access, including their hidden files and linked files; see Security.
Run each example separately, stopping a running server with Ctrl+C before starting another.
Serve a different directory on port 8080 (also useful when port 8000 is already in use):
www -r /path/to/files -p 8080Open http://localhost:8080/. Replace /path/to/files with an existing directory.
For a local preview accessible only from your own machine:
www --bind 127.0.0.1 -r ./public -p 8080Open http://127.0.0.1:8080/. Here and in later examples, replace ./public with your site's existing directory.
To suppress startup and request logs:
www -q -r ./public- Static file streaming with
HEAD, byte-range (206 Partial Content), and conditional (If-Modified-Since) support. - Automatic
index.htmlserving and HTML-escaped, sorted directory listings. - Instant self-signed ECDSA HTTPS for
localhost(127.0.0.1and::1). - Automatic Let's Encrypt HTTPS via TLS-ALPN-01 on port 443 with background renewal and private atomic certificate caching.
- Dual-stack IPv4/IPv6 listener with
TCP_NODELAYenabled across HTTP, localhost HTTPS, and ACME HTTPS. - Pure Rustls TLS stack with zero OpenSSL or native-TLS dependencies.
| Option | Behavior |
|---|---|
-p PORT |
Listen on this port; default 8000 |
--bind IP |
Bind a numeric IPv4 or IPv6 address; default all interfaces |
-r PATH |
Serve this directory; default current directory (.) |
-q |
Suppress startup and request logs; errors remain visible |
-s localhost |
Generate an in-memory self-signed ECDSA certificate and use the selected port |
-s DOMAIN |
Obtain and renew a Let's Encrypt certificate; always listens on port 443 |
--cert-cache PATH |
Override persistent ACME account and certificate storage |
--acme-staging |
Use Let's Encrypt staging for domain testing |
-h, --help |
Show help |
-V |
Show the package version |
--version |
Show the package version and source commit hash |
www -s localhost -r ./public -p 8080Open https://localhost:8080/ in your browser. A certificate warning is expected because the certificate is self-signed; proceed only if you trust this local server. To test from a second terminal:
curl --insecure https://localhost:8080/A fresh ECDSA P-256 certificate covering localhost, 127.0.0.1, and ::1 is generated in memory on each start. Because it is self-signed, clients require an explicit trust decision.
- Point your domain's A record (for example
share.example.com) to the server's public IPv4 address. Add an AAAA record only when public IPv6 reaches the same server. - Allow incoming TCP port 443 directly to
www(no TLS-terminating reverse proxy or CDN proxy mode). - Ensure the process has permission to bind port
443(for exampleCAP_NET_BIND_SERVICEon Linux).
Run with your domain to obtain a publicly trusted certificate. Let's Encrypt production is the default; no staging flag is needed:
www -s share.example.com -r ./publicwww answers TLS-ALPN-01 challenges directly on port 443 and renews certificates automatically while running; port 80 and external cron jobs are unnecessary.
--cert-cache PATH selects persistent storage for ACME account keys, certificate private keys, and issued certificates. It requires -s DOMAIN; -s localhost generates its certificate in memory.
For a manual run from a checkout with an explicit cache, build as your normal user, then use sudo to bind port 443:
just build
sudo ./target/release/www -s share.example.com \
-r ./public --cert-cache /var/lib/www/acmeReplace share.example.com with your domain and ./public with the directory you intend to share. Press Ctrl+C to stop. Restart with the same cache path and service account to reuse the account and certificate. For deployments, keep the cache in persistent storage, including across container replacements.
The running user must be able to create and write the cache. New cache directories use 0700 and atomically written files use 0600 on Unix; existing directories must already have owner-only permissions. A cache created by sudo contains root-owned files. Treat the cache as private key material and keep it outside your public document root.
Without --cert-cache, storage defaults to the running user's data directory:
- Linux and FreeBSD:
$XDG_DATA_HOME/www/acmeor$HOME/.local/share/www/acme - macOS:
$HOME/Library/Application Support/io.nbari.www/acme
Preserve the cache across restarts to avoid unnecessary issuance.
Use --acme-staging when testing or debugging certificate issuance:
www -s share.example.com -r ./public --acme-stagingThis selects Let's Encrypt's test CA. It exercises real certificate issuance and TLS-ALPN-01 validation, with the same DNS and public TCP/443 requirements, but its certificates are not trusted by browsers or ordinary HTTPS clients.
Omit --acme-staging for normal HTTPS with a publicly trusted certificate. Staging and production can use the same cache directory because their cache keys are isolated; switching to production uses a cached production certificate or obtains one if needed.
On first start, HTTPS handshakes can fail until issuance finishes. Wait for DeployedNewCert or DeployedCachedCert in the server log, then run from a remote machine:
curl -v https://share.example.com/
curl -I https://share.example.com/For a staging test, add -k to skip verification of its untrusted certificate, for example curl -vk https://share.example.com/. Production certificates should work without -k.
To select a protocol explicitly, add --http1.1 or --http2 (if your curl build supports HTTP/2). Keep the domain in the URL so the client sends the expected TLS server name.
Serving a file is one filesystem step: the path is resolved, the handle is opened, and a body up to 256 KiB is read, all in a single operation. Bodies larger than that stream in 128 KiB chunks.
That step normally runs directly on a runtime worker, because on a warm page cache the thread handoff costs more than the syscalls do. Two limits keep a slow filesystem from turning that into a stall:
- At most half the runtime workers may be inside a filesystem operation at once, so accepting connections, timers, ACME renewal and shutdown keep running even against a mount that never answers. A single-worker runtime therefore never reads inline, and always uses the blocking pool.
- If any resolution takes longer than 1 ms, every resolution moves to the blocking pool for the next second, then the fast path is retried with a single probe.
The effect scales with the number of workers. Serving a 1 KiB file, measured against 3.0.1 on the same host:
| Server CPUs / workers | 3.0.1 | 3.1.0 | |
|---|---|---|---|
| 1 | 27,853 req/s | 43,379 req/s | 1.56x |
| 2 | 49,205 req/s | 111,737 req/s | 2.27x |
| 4 | 81,974 req/s | 230,465 req/s | 2.81x |
Streaming bodies share a 64 MiB memory ceiling. Past it, further responses stream in 16 KiB chunks rather than waiting or failing, so the memory a set of slow readers can pin grows more slowly than the number of connections.
just check
just acmejust checkruns formatting, strict Clippy lints, the Rustls-only dependency gate, unit/integration tests, and benchmark safeguard unit tests.just acmeruns TLS-ALPN-01 issuance, idle renewal, cached restart, and CA outage recovery against an in-process Rust test CA. No external service or container runtime is required.just coveragegenerates HTML and LCOV reports usinggrcov; install its prerequisites withjust coverage-tools. See code coverage.ACME_DOMAIN=example.com ACME_INGRESS_PROBE_CMD='...' just acme-liveruns opt-in live Let's Encrypt staging verification after fail-closed public DNS, egress, port-443 bind, and external ingress nonce checks (see Live staging verification).
See CONTRIBUTING.md for MSRV verification, live ACME probe protocol, Linux static musl packaging, FreeBSD cross-compilation, and release workflows.
Measure local HTTP and localhost HTTPS throughput and latency on Linux using oha (built with Rustls):
just bench-tools
just bench
just bench-http
just bench-httpsThe benchmark runner (examples/bench.rs) builds the release binary, generates temporary 1 KiB, 100 KiB, 4 MiB, and 200-entry directory fixtures, pins server and client CPU sets with taskset, and measures GET, HEAD, conditional (304), and byte-range (206) workloads over HTTP and localhost HTTPS with keep-alive enabled and disabled, comparing TCP_NODELAY enabled vs. disabled across concurrencies 1, 16, 64, and 128. Besides requests/sec, throughput, latency percentiles, CPU, and RSS it records per-request read/write syscalls and context switches from /proc, summed across every server thread.
The load generator never runs on an SMT sibling of a server CPU, so the two processes cannot compete for the same physical cores. Samples that saw an unexpected status or a transport error stay in the raw output but are excluded from published medians and ratios. The nagle variant applies only to the rust implementation, the one that reads WWW_BENCH_DISABLE_TCP_NODELAY; external binaries are measured under their own default socket configuration.
Any other prebuilt server that accepts -p <port> -r <root> -q [-s localhost] can be benchmarked side by side with --server LABEL=PATH (repeatable); --servers LIST selects and orders the labels to run. The implicit rust label is always the working-tree release build:
just bench --server go=/tmp/www-go --servers rust,goResults are grouped per case and implementation (medians across rounds, never averaged across cases or implementations) and include a rust/go req/s ratio when both labels run. Raw oha JSON, results.json, samples.csv, and summary.txt are written under target/benchmarks/.
docker build -t www .
docker run --rm -p 8000:8000 -v "$PWD:/srv:ro" wwwwww binds all IPv4 and IPv6 interfaces ([::]:port with dual-stack fallback to 0.0.0.0:port) by default. Use --bind 127.0.0.1 or --bind ::1 for loopback access. Explicit addresses bind only their selected family; IPv6 addresses do not accept IPv4 connections. Hostnames and address:port forms are not accepted; select the port with -p. Domain HTTPS still uses port 443, and the selected interface must receive public ACME validation traffic. Hidden files (dotfiles) are served and ordinary symbolic links are followed, including links pointing outside the document root; share only directories intended for public access. Path traversal (..), NUL bytes, backslashes, and the active ACME certificate cache directory (including symlink aliases to it) are rejected.
Security support starts with version 3. Report vulnerabilities privately as described in the security policy.