Minimal Rust HTTP server for static files in Kubernetes.
- Serves static files from
/app/publicby default - Supports
GETandHEAD - Resolves
index.htmlfor/,/dir, and/dir/ - Blocks path traversal and symlink escapes outside the content root
- Opens resolved files during path resolution to avoid a resolve/open TOCTOU gap
- Exposes Kubernetes probes:
/livez/readyz
- Shuts down gracefully during rollouts
- Emits structured telemetry via
telemetry-setup - Records request metrics for count, duration, response body size, and active requests
No TLS, auth, uploads, directory listings, reverse proxying, compression, range requests, ETags, or dynamic content.
- Rust + Cargo
- Podman for container builds
The reference container build targets x86_64-unknown-linux-musl and produces a static binary for a scratch image.
mkdir -p public
printf 'hello\n' > public/index.html
cargo run -- --listen-addr 127.0.0.1:8080 --content-root ./publicEnvironment variables can be overridden by CLI flags.
| Setting | Environment variable | CLI flag | Default |
|---|---|---|---|
listen_addr |
TINY_HTTPD_LISTEN_ADDR |
--listen-addr |
0.0.0.0:8080 |
content_root |
TINY_HTTPD_CONTENT_ROOT |
--content-root |
/app/public |
service_name |
TINY_HTTPD_SERVICE_NAME |
--service-name |
tiny-httpd |
Startup fails if the content root is missing or not a directory, the socket cannot be bound, or telemetry setup fails.
| Request | Result |
|---|---|
GET /path |
Returns the file body |
HEAD /path |
Returns the same headers without the body |
| Other methods | 405 Method Not Allowed with Allow: GET, HEAD |
Any method on /livez |
Liveness status |
Any method on /readyz |
Readiness status |
Successful file responses include Content-Type (derived from the file extension via mime_guess) and Content-Length. Probe responses include Content-Type: text/plain; charset=utf-8.
| Case | Status |
|---|---|
| Malformed request target or invalid percent encoding | 400 Bad Request |
| Path traversal attempt or content-root escape | 400 Bad Request |
| File not found | 404 Not Found |
| Unsupported method | 405 Method Not Allowed |
| I/O error while serving an existing file | 500 Internal Server Error |
Error bodies are plain text and intentionally minimal.
| Path | Lookup |
|---|---|
/ |
index.html |
/foo |
foo, then foo/index.html if foo is a directory |
/foo/ |
foo/index.html |
/foo.html |
foo.html |
Probe routes take precedence over static files.
livenessProbe:
httpGet:
path: /livez
port: http
readinessProbe:
httpGet:
path: /readyz
port: httpDuring graceful shutdown the server:
- Marks readiness false and rejects non-probe requests with
503. - Keeps the listener open for a 250 ms readiness drain window so
/readyzcan return503before the socket closes. - Stops accepting new connections and drains in-flight requests up to a 10 s hard timeout, after which remaining tasks are aborted.
/livezstays200until process exit.
This gives Kubernetes an HTTP readiness failure signal before connection refusal.
A .containerignore excludes target/, .git/, and .gitignore from the build context to keep image builds smaller and preserve layer caching.
Build from the repository root:
podman build -f Containerfile -t tiny-httpd:dev .The final image contains:
/app/tiny-httpd
/app/public/...
The public/ directory must exist at the repository root before building; the COPY step fails otherwise. Add site files there, or use the init-container pattern below.
When server and content versions need independent release cadence, package content in a separate immutable image and copy it into a shared volume before tiny-httpd starts:
initContainers:
- name: content
image: site-content:<content-digest-or-tag>
command: ["/bin/cp", "-a", "/public/.", "/app/public/"]
volumeMounts:
- name: public
mountPath: /app/public
containers:
- name: tiny-httpd
image: tiny-httpd:<server-version>
volumeMounts:
- name: public
mountPath: /app/public
readOnly: true
volumes:
- name: public
emptyDir: {}The copy tool belongs to the init container or content image, not to tiny-httpd; the server image stays minimal. Runtime content fetching from object storage, Git, or other network sources is out of scope; such workflows must complete before tiny-httpd starts and must present a local immutable content_root.
The server emits per-request spans and HTTP server metrics.
| Instrument | Type | Unit | Description |
|---|---|---|---|
http.server.request.count |
Counter | — | Completed HTTP requests |
http.server.request.duration |
Histogram | s | HTTP request duration |
http.server.response.body.size |
Histogram | By | HTTP response body size |
http.server.active_requests |
UpDownCounter | — | HTTP requests currently in flight |
Completed-request metrics include these attributes:
http.request.methodhttp.response.status_class
Response body size is recorded from the HTTP Content-Length header when present. In this server, file and text responses set a fixed content length, so the recorded metric reflects the declared response body size for both GET and HEAD handling.
cargo fmt
cargo clippy -- -D warnings
cargo test
cargo build --release