👋 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.html with 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/.gz alongside 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

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