Spars - A low-feature, lightweight, HTTP 1.1 server for serving static files and SPAs
Spars is a lightweight (both binary size and memory usage) HTTP server optimized for safely and securely serving static files and Single Page Applications, particularly within containers.
Spars was written because I was annoyed at seeing so many nginx worker processes in the ps output of my homelab, serving random static websites. decided to use the opportunity to better understand http servers and the Rust language.
Features
- Serve static files from filesystem
- Index and 404 file support
- Mime-Type detection based on file extension (common extensions only by
default, expanded support available via the
mime_guesscargo feature) - Configuration via ENV vars
- Security: Ignore hidden files by default (except /.well-known/ directory)
- Security: Files served via allowlist to eliminate path traversal attacks
- Security: Rust Memory Safely Guarantees
Non-Features
Spars is intended for use behind a full-featured http proxy (with logging, etc) for low-qps static sites. It deliberately does not implement:
- Logging
- HTTP methods beyond GET/HEAD
- Live file updates
- Authentication
It is not expected that these features will be added to Spars unless they can be done without increasing binary size and/or memory usage. If you are looking to serve a high-QPS site, or needing any of these features, you should use a well-known httpd (nginx, apache, etc) instead.
Benchmarks
- Data collected unscientifically by running
wrk -t12 -c12 -d10s http://127.0.0.1:3000/index.htmlon my low powered homelab (AMD Ryzen 7 4800U) and collecting the output from/proc/<pid>/status. - "Hyper" data collected using the hyper.rs example. The hyper example uses the
same handler as spars, but is driven by
hyper::http1::Builder::serve_connection(). - Nginx data collected by inspecting random nginx processes running on my homelab and picking the lowest resource usage.
| Httpd Implementation | QPS | Disk Size | Processes | VmRSS | VmHWM | VmSize | VmPeak |
|---|---|---|---|---|---|---|---|
| Spars - Plaid + 8kB stack | 267.03 | 136K | 1 | 1396 kB | 1396 kB | 593040 kB | 593136 kB |
| Spars - Plaid + musl + 8kB stack | 265.84 | 172K | 1 | 328 kB | 368 kB | 500 kB | 788 kB |
| Spars - Docker spars:latest | ^ | 98K | 1 | ^ | ^ | ^ | ^ |
| Spars - Release | 266.24 | 644K | 1 | 1372 kB | 1372 kb | 679432 kB | 745064 kB |
| Spars - Release + musl | 265.24 | 740K | 1 | 796 kB | 796 kB | 3112 kB | 17644 kB |
| Hyper - Plaid | 267.94 | 260K | 1 | 2472 kB | 2472 kB | 1693960 kB | 1759484 kB |
| Hyper - Plaid + musl | 267.43 | 304K | 1 | 296 kB | 620 kB | 52412 kB | 52764 kB |
| Hyper - Docker spars:examples-hyper | ^ | 148K | 1 | ^ | ^ | ^ | ^ |
| Hyper - Release | 268.51 | 988k | 1 | 2992 kB | 2992 kb | 1694796 kB | 1694796 kB |
| Hyper - Release + musl | 267.52 | 1.1M | 1 | 984 kB | 1368 kB | 51124 kB | 51480 kB |
| Nginx Default Docker Config | - | 21M | 32 | 49864 kB | 51764 kB | 168568 kB | 168568 kB |
Plaid Builds
Barf: What the hell was that?
Lone Starr: Spaceball 1.
Barf: They've gone to plaid!
~ Spaceballs, 1987
The plaid build profile uses many of the techniques from johnthagen's min-sized-rust repo to optimize the release build, reducing the size of the final binary. This results binaries that are 4x smaller and minimally faster, but makes debugability impossible.
Using spars
Configuration
Spars is configured via ENV vars.
ROOT- Root dir where files will be served from
- Default:
./public(/publicwithin Docker)
INDEX_FILE- Filename read when a path ending in a slash is requested
- Default:
index.html
FALLBACK_PATH- HTTP path loaded when the requested path does not exist
- Fallback responses are sent with the
200 OKstatus code - When set to an empty string or a the path does not exist, empty 404 responses will be sent instead
- Default:
/404.html
ALLOW_HIDDEN- Whether spars should serve files/directories starting with
.false|0: Files where any path component starts with.will not be servedtrue|1: Files where any path component starts with.will be served (DANGER!)wellknown| Paths starting with/.well-known/will be served, unless a following component starts with.
- Default:
wellknown
- Whether spars should serve files/directories starting with
ADDR- Interface the http server will bind to
- Supports both ipv4 and ipv6 values
- Default:
127.0.0.1(0.0.0.0within Docker)
PORT- Port the http server will bind to
- Spars can be bound to a random available port by setting a value of
0, and accessed by reading the file specified by theADDR_FILEvar. - Default:
3000
PID_FILE- If set, spars will write its process id to this file on startup
- Default: unset
ADDR_FILE- If set, spars will write
<IP>:<PORT>to this file upon binding the tcp listener - Default: unset
- If set, spars will write
Docker
This repo includes a Dockerfile for building spars. This Dockerfile:
- Builds spars
- With the
signal-handlerfeature, allowing it to be used as a container init processes - Using the plaid profile, reducing binary size
- Against the
x86_64-unknown-linux-musltarget, producing a static binary
- With the
- Compresses it with upx, further reducing the binary size
- Configures spars with the following overridable ENV defaults:
ADDR:0.0.0.0PORT:3000ROOT:/publicINDEX_FILE:index.htmlFALLBACK_PATH:/404.htmlALLOW_HIDDEN:wellknown
Planned work: publish spars to DockerHub and link it here.