nano-web 1.1.3

Static file server built with Rust with pre-compressed in-memory caching
Documentation

nano-web

CI Build Crates.io License: MIT

Static file server built with Rust. Pre-loads and pre-compresses all files at startup to be served with near-zero latency or waiting for disk-caching.

Performance

  • Axum/Hyper HTTP stack
  • Files pre-compressed at startup (brotli/gzip/zstd)
  • Lock-free concurrent HashMap routing
  • Zero-copy serving with Bytes

Benchmark (M3 Max 36GB):

wrk -c 50 -d 10 -t 50 http://localhost:3000
Running 10s test @ http://localhost:3000
 50 threads and 50 connections
 Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency   328.63us   47.98us   2.86ms   88.05%
   Req/Sec     3.01k   103.63     3.21k    91.58%
 1513328 requests in 10.10s, 8.58GB read
Requests/sec: 149838.48
Transfer/sec:    870.24MB

I know web-server benchmarks are mostly useless, however, this server does very little, and this is it's literal use case (serve file quick!).

Based on trying other high-performance servers locally, I reckon nano-web would place in the top ten in the TechEmpower plaintext webserver benchmarks if ran on their hardware (haven't got around to submitting it yet). It beats out may-minihttp, for instance.

Features

  • In-memory file serving with compression
  • SPA mode with index.html fallback
  • Dev mode with file watching
  • Health endpoint at /_health
  • Runtime environment variable injection
  • JSON/console logging
  • Docker image size of ~5MB

📦 Installation

Install with Mise (via ubi)

mise install ubi:radiosilence/nano-web

Install with Cargo

cargo install nano-web

Download Binary

Pre-built binaries available on GitHub Releases.

🐳 Docker

Multi-arch images available:

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

Production example:

FROM node:lts-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM ghcr.io/radiosilence/nano-web:latest
COPY --from=builder /app/dist/ /public/
EXPOSE 3000

🔧 Usage

# Serve files from ./public/ on port 3000
nano-web serve

# Custom directory and port
nano-web serve ./dist --port 8080

# SPA mode with dev reloading
nano-web serve --spa --dev --port 3000

# See all options
nano-web serve --help

⚙️ Configuration

Variable CLI Flag Default Description
PORT --port, -p 3000 Port to listen on
--spa --spa false Enable SPA mode (serve index.html for 404s)
--dev --dev, -d false Enable dev mode (hot-reload files)
CONFIG_PREFIX --config-prefix VITE_ Environment variable injection prefix
LOG_LEVEL --log-level info Logging: debug, info, warn, error
LOG_FORMAT --log-format console Format: json or console
LOG_REQUESTS --log-requests false Enable request logging

Environment Variables

# Docker example
docker run -p 3000:3000 -e PORT=3000 -e SPA_MODE=true ghcr.io/radiosilence/nano-web:latest

⚡ Runtime Environment Injection

Inject configuration at runtime without rebuilding, so you can re-use the same image for different things quickly and easily (or distribute it).

<!-- Your index.html -->
<script type="module">
  window.ENV = JSON.parse("{{EscapedJson}}");
  // or direct injection if you don't mind the broken syntax:
  window.ENV = {{Json}};
</script>
// env.ts
import * as z from "zod";
const EnvSchema = z.object({
  API_URL: z.url(),
});
// Parse and validate environment variables
const result = EnvSchema.safeParse(window.ENV);

if (!result.success) {
  throw new Error(
    `Environment validation failed:\n${z.prettifyError(result.error)}`,
  );
}
export const { API_URL } = result.data;
# 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

Template Engine

Uses MiniJinja template syntax for environment variable injection. Variables available:

  • {{env.VARIABLE_NAME}} - Direct variable access
  • {{Json}} - Raw JSON string of all prefixed variables
  • {{EscapedJson}} - JSON-escaped for inline JavaScript

Templating is run at startup and cached.

🏥 Health Checks

Built-in health endpoint at /_health:

{ "status": "ok", "timestamp": "2025-01-15T10:30:45Z" }

📊 Logging

Console format (default):

2025-08-12T18:15:00.990620Z  INFO nano_web::routes: Processing 18 files in parallel
   at src/routes.rs:70

 2025-08-12T18:15:01.207449Z  INFO nano_web::routes: Routes populated: 20 routes
   at src/routes.rs:102

 2025-08-12T18:15:01.207466Z  INFO nano_web::server: Routes loaded: 20
   at src/server.rs:46

 2025-08-12T18:15:01.207564Z  INFO nano_web::server: Starting server on 0.0.0.0:3001
   at src/server.rs:53

JSON format for log aggregation:

{"timestamp":"2025-08-12T18:16:23.667689Z","level":"INFO","fields":{"message":"Processing 18 files in parallel"},"target":"nano_web::routes"}
{"timestamp":"2025-08-12T18:16:23.878820Z","level":"INFO","fields":{"message":"Routes populated: 20 routes"},"target":"nano_web::routes"}
{"timestamp":"2025-08-12T18:16:23.878843Z","level":"INFO","fields":{"message":"Routes loaded: 20"},"target":"nano_web::server"}
{"timestamp":"2025-08-12T18:16:23.878998Z","level":"INFO","fields":{"message":"Starting server on 0.0.0.0:3001"},"target":"nano_web::server"}

🛠️ Building from Source

# Clone and build
git clone https://github.com/radiosilence/nano-web.git
cd nano-web
cargo build --release

# Run tests
cargo test

# Run benchmarks
cargo bench

Development

# Development server with hot-reload
cargo run -- serve ./public --dev --spa

# Watch for changes
cargo watch -x "run -- serve ./public --dev"

Compared to previous Go version: 80% faster (150k vs 76k req/sec), lower latency.

📄 License

Licensed under the MIT License - see LICENSE for details.

🙏 Acknowledgments

  • Axum - Ergonomic async web framework
  • Hyper - Fast HTTP implementation
  • Tokio - Asynchronous runtime
  • DashMap - Lock-free concurrent HashMap
  • Brotli - Compression library