π Hi everyone!
I'm excited to share Wisp β originally codenamed LWS (Lightweight Web Server) β a tiny, purpose-built static file server written entirely in Go, using only the standard library. No third-party dependencies, no bloat, just a focused, secure, and blazing-fast service that gets straight to work. β¨
π Repository: github.com/appex-uk/lws
π¦ What Makes It Special
Wisp started from a simple frustration: most web servers are either huge and complex or too limited for modern deployments. I wanted something that does exactly what you need β nothing more, nothing less β and fits comfortably in even the tightest environments.
π€ Itβs genuinely tiny
- Scratch container image: ~3 MB β yes, megabytes
- Zero runtime dependencies β statically compiled Go binary
- Starts instantly β before your health check timeout can even fire
- Minimal memory footprint β your resources stay yours
π οΈ Capable where it counts
Don't let the size fool you β Wisp is packed with thoughtful features:
| Feature | Details |
|---|---|
| π Secure static serving | Uses os.Root to confine all requests β paths and symlinks can never escape your content directory |
| π« Hidden file protection | Dotfiles and directories are never served; .well-known/ works as expected |
| π¦ Precompressed content | Serve .br (Brotli) or .gz files directly β compress once at build time |
| π SPA & custom routing | SPA fallback mode, custom 404 pages, per-path header rules |
| π Built-in Prometheus metrics | Separate metrics port β no exporters, no SDKs, just stdlib |
| π©Ί Kubernetes-ready probes |
/livez and /readyz endpoints with graceful shutdown drain |
| π Custom headers & security | Set cache policies, security headers, or anything else β globally or per path |
| β±οΈ Graceful shutdown | Drains connections before terminating β no dropped requests |
π Quick Start
Run it locally
# Clone and run
git clone [https://github.com/appex-uk/lws.git](https://github.com/appex-uk/lws.git)
cd lws
make run
This serves the included ./www directory on port 8080.
Docker β the scratch image magic β¨
# Build the image yourself
make docker
# Run with your own content and config
docker run --rm -p 8080:8080 -p 9090:9090 \
-v "$PWD/site:/srv/www:ro" \
-v "$PWD/config.json:/etc/lws/config.json:ro" \
lws:latest
That's it. No pulling hundreds of MB of layers β just a tiny, secure container that starts in milliseconds.
βοΈ Configure It Your Way
Everything lives in a simple JSON config file. Every field is optional β sensible defaults apply:
{
"address": ":8080",
"root": "/srv/www",
"fallback": "/index.html",
"not_found_page": "/404.html",
"precompressed": true,
"headers": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY"
},
"header_rules": [
{"match": "*.html", "headers": {"Cache-Control": "no-cache"}},
{"match": "/assets/*", "headers": {"Cache-Control": "public, max-age=31536000, immutable"}}
],
"access_log": true
}
Key highlights:
-
SPA mode β missing routes without an extension get your
index.htmlwith status 200; missing files with an extension still 404 β so broken assets don't silently fail - Per-path headers β fine-grained cache control or security policies
-
Precompressed β ship
.br/.gzalongside originals and Wisp serves the best one automatically
π Built-in Observability β Zero Dependencies
This is one of my favourite parts. Wisp exposes a Prometheus metrics endpoint β on a separate, non-public port β all written with pure Go stdlib. No Prometheus client library, no OTel SDK, nothing to pull in.
Available at: :9090/metrics (configurable)
What you get:
-
lws_http_requests_totalβ request count by method & status code -
lws_http_request_duration_secondsβ latency histogram -
lws_http_requests_in_flightβ concurrent requests gauge -
lws_http_response_bytes_totalβ bytes served - Plus standard Go runtime & process metrics
Some useful queries you can run straight away:
# Traffic rate
sum(rate(lws_http_requests_total[5m]))
# Error rate
sum(rate(lws_http_requests_total{code=~"5.."}[5m]))
/
sum(rate(lws_http_requests_total[5m]))
# 99th percentile latency
histogram_quantile(0.99, sum by (le) (rate(lws_http_request_duration_seconds_bucket[5m])))
Instrumentation overhead? ~170 ns per request. Negligible.
βΈοΈ Kubernetes Friendly by Design
Wisp was built to run reliably in Kubernetes:
-
Liveness probe:
/livezβ just checks the process is responding -
Readiness probe:
/readyzβ also verifies the content volume and goes 503 during shutdown drain - Graceful sequence on SIGTERM: β readiness 503 β finish in-flight requests β clean exit
- Example Deployment and Service manifests are in the repo under
infra/k8s/
This means rolling deployments work smoothly β connections drain before the pod disappears.
π Why Not Just Nginx?
Great question β and to be clear: Nginx is amazing. It's battle-tested, incredibly capable, and I use it every day. But sometimes it's simply more than you need:
π Nginx = The workhorse β handles massive traffic, complex routing, modules, SSL termination at scale β and it's great at that.
π² Wisp = The nimble specialist β serve static files securely, fast, and tiny. Perfect for frontend deployments, edge workloads, sidecars, or anywhere "light and fast" matters more than "every feature ever."
If you need to serve a static site, a single-page app, or static assets reliably and securely β and you care about image size, startup time, and minimal resource consumption β Wisp is built exactly for you.
πΊοΈ What's Next
Right now Wisp is focused on doing one thing β serving static content β and doing it really well. That said, there's always room to polish and improve:
- More flexible routing options
- Additional cache control conveniences
- Real-world feedback from folks using it
Your thoughts, bug reports, or contributions are very welcome!
π Links
- GitHub repo: github.com/appex-uk/lws β Star if you like it!
- Issues & feedback: Open an issue β I'd love to hear what you think
π¬ Over to you
What's your biggest annoyance when setting up a static file server? Is it image bloat? Complex config? Missing metrics? Let's chat in the comments β I'm here to learn and build something useful together! π
Top comments (0)