β‘ 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.
- π 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.
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.
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π
Instead of rebuilding your app for different environments, inject configuration at runtime:
<!-- 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
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 |
Enabled by default at /_health
:
{"status":"ok","timestamp":"2025-05-27T08:19:32Z"}
go install github.com/radiosilence/nano-web@latest
# 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
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
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
}
Install Task for build automation:
# macOS
brew install go-task/tap/go-task
# Linux/Windows - see https://taskfile.dev/installation/
# 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
This project is licensed under the MIT License - see the LICENSE file for details.
- FastHTTP - Fast HTTP library
- Zerolog - Structured logging library
- Brotli - Compression algorithm
- Zstandard - Fast compression algorithm with excellent compression ratios