👋 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! 👇