Skip to content

radiosilence/nano-web

Β 
Β 

Repository files navigation

πŸ”₯ nano-web

publish-image push-package-amd64 release test Go Report Card License: MIT

⚑ Hyper-minimal, lightning-fast web server for SPAs and static content

Built on FastHTTP, nano-web is designed for minimal latency. Purpose built for use with containerized deployments/unikernel environments with immutable content, however totally useable as a local CLI server.

✨ What makes nano-web different

  • πŸš„ Ridiculously low latency - Pre-caches everything in memory precompressed with zstd/brotli/gzip where appropriate, serves 100k+ requests/second with sub-millisecond latency.
  • πŸ“¦ Tiny footprint - Tiny (<20MB) Docker image.
  • πŸ”§ Runtime environment injection - Safely inject environment variables at runtime, so you can configure containers without rebuilding if you don't want to do different builds for things, or want to test your prod image against a different environment.
  • πŸš‘ Inbuilt Healthchecks - Available at /_health.
  • 🎯 SPA-mode - Supports modern single-page applications with fallback routing.
  • ⚑️ Fast builds - Building an image from nano-web is extremely fast because it is tiny.

πŸ“ˆ Performance

nano-web pre-caches everything in memory with compression, which makes it fast. Sure you could rely on filesystem caching to do this, but knowing the content ahead of time allows us just to compress everything and stick it in RAM. Benchmark on a M3 Max 36GB:

wrk -d 10 -c 20 -t 10 http://localhost
  1,012,393 requests in 10.10s, 7.12GB read
Requests/sec: 100,237
Transfer/sec: 721MB/s
Latency: 200ΞΌs avg (96.93% consistency)

The trade off is basically to use more memory at startup to do less work on each request due to having predictable content. Generally it shouldn't use that much more RAM than the project by much.

🐳 Docker

FROM ghcr.io/radiosilence/nano-web:latest
COPY ./dist /public/

Real-world example for running as unprivileged user (/bin/sh etc are not available for the runner):

FROM oven/bun:1 AS base
RUN adduser --disabled-password --shell /bin/sh nano
WORKDIR /app

FROM base AS deps
COPY package.json ./
RUN bun install

FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build
RUN chown -R nano:nano /app/.output/public/

FROM ghcr.io/radiosilence/nano-web:latest AS runner
COPY --from=base /etc/passwd /etc/passwd
COPY --from=base /etc/group /etc/group
COPY --from=builder /app/.output/public/ /public/
USER nano
ENV PORT=3000
EXPOSE 3000

This is a TanStack Start project being built statically with bun.

Configure with env vars you can see belowπŸ‘‡

πŸ”§ Runtime Environment Injection

Instead of rebuilding your app for different environments, inject configuration at runtime:

⚠️ Public config only - don't put secrets here.

<!-- Your index.html -->
<script type="module">
  // You can wrap the escaped JSON in a string and parse it.
  window.ENV = JSON.parse("{{.EscapedJson}}");
  // You can pass in the raw JSON, but this means having the template tag directly inserted without
  // being quoted.
  window.ENV = {{.Json}};
</script>
import { z } from "zod";
// Your React/Vue/whatever app
const EnvSchema = z.object({
  API_URL: z.string().optional(),
});
const { API_URL } = EnvSchema.parse(window.ENV));
# Same build, different configs
docker run -e VITE_API_URL=http://localhost:3001 my-app    # dev
docker run -e VITE_API_URL=https://api.prod.com my-app    # prod

βš™οΈ Configuration

Variable CLI Flag Default Description
PORT --port 80 Port to listen on
SPA_MODE --spa-mode false Enable SPA mode (serve index.html for 404s)
DEV --dev false Enable Dev mode (check for file changes when serving files)
CONFIG_PREFIX --config-prefix VITE_ Prefix for runtime environment variable injection
LOG_LEVEL --log-level info Logging level: debug, info, warn, error
LOG_FORMAT --log-format console Log format: json or console
LOG_REQUESTS --log-requests true Enable/disable request logging

πŸš‘ Health checks

Enabled by default at /_health:

{"status":"ok","timestamp":"2025-05-27T08:19:32Z"}

πŸ“Ί CLI Usage

Install via Go

go install github.com/radiosilence/nano-web@latest

Usage Examples

# Basic usage - serve files from ./public/ on port 80
nano-web serve

# Serve files from custom directory on port 8080
nano-web serve ./dist --port 8080

# Enable SPA mode with custom configuration, file reloading, and debug logging (similar to `task dev`)
nano-web serve ./build --port 3000 --spa-mode --dev --log-level debug

# See all available options
nano-web --help
nano-web serve --help

# Show version
nano-web version

🌰 Nanos/OPS Microkernels

You will want to make a config that looks something like this:

{
  "Dirs": ["public"],
  "Env": {
    "SPA_MODE": "1",
    "PORT": "8081"
  },
  "RunConfig": {
    "Ports": ["8081"]
  }
}

And then you can build your unikernel image:

# Build the unikernel image
ops image create -c config.json --package radiosilence/nano-web:latest -i my-website

# Test locally
ops instance create my-website -c ./config.json --port 8080

# Deploy to cloud
ops instance create my-website -c ./config.json -t gcp

πŸ“Š Logging

Defaults to readable, colourful style (--log-format console):

9:15AM INF routes populated successfully route_count=3
9:16AM INF request handled bytes=21 duration=0.044208 method=GET path=/ status=200

Structured JSON for consumption by logging platforms such as DataDog etc (--log-format json) (enabled by default in docker):

{
  "level": "info",
  "time": "2024-01-15T10:30:45Z",
  "message": "request served",
  "method": "GET",
  "path": "/",
  "status": 200,
  "duration_ms": 1.2
}

πŸ—οΈ Building from Source

Prerequisites

Install Task for build automation:

# macOS
brew install go-task/tap/go-task

# Linux/Windows - see https://taskfile.dev/installation/

Building

# Clone the repository
git clone https://github.com/radiosilence/nano-web.git
cd nano-web

# See all available tasks
task

# Build for current platform
task build

# Build for all platforms
task build-all

# Run tests
task test

# Run tests with coverage
task test-coverage

# Run benchmarks
task bench

# Development server with reloading and debug logging
task dev

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • FastHTTP - Fast HTTP library
  • Zerolog - Structured logging library
  • Brotli - Compression algorithm
  • Zstandard - Fast compression algorithm with excellent compression ratios

Made with πŸ–€ by @radiosilence

About

πŸ”₯ Ultra-fast in-memory static file server for SPAs and static content

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages

  • Go 95.9%
  • Dockerfile 3.5%
  • Shell 0.6%