- Rust 89.7%
- Nix 10.3%
|
|
||
|---|---|---|
| .moira | ||
| .woodpecker | ||
| grafana | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| renovate.json | ||
⛩️ sh3rine
Stateless async S3 static site proxy. Point it at a Ceph RGW (or any S3-compatible) endpoint, map hostnames to buckets, and it handles all the edge cases that raw S3 path-style access gets wrong.
Why
S3's native static website hosting requires a separate endpoint per bucket and doesn't exist in self-hosted Ceph without significant extra setup. Doing the routing in a reverse proxy (Caddy rewrites, nginx try_files) is fragile and hard to reason about. sh3rine is a small dedicated server that does exactly one thing cleanly.
Features
- Smart path resolution — extensionless URLs try
.html→index.html→ exact path, in that order - Custom 404 pages — serves
404.htmlfrom the bucket root before falling back to a plain 404 - Correct Content-Type — overrides S3's
application/octet-streamusing file extension, with byte sniffing fallback (handles extensionless AVIF, WebP, etc.) - LRU cache — small files cached in memory with TTL; large files always streamed
- Dynamic host → bucket mapping — exact matches and regex patterns with named capture groups
- Strips S3 noise — drops
content-encoding: aws-chunkedand other S3-internal headers
Configuration
All config via environment variables.
| Variable | Default | Description |
|---|---|---|
ENDPOINT |
http://localhost:9000 |
S3/RGW base URL |
LISTEN |
0.0.0.0:8080 |
Bind address |
HOSTS |
— | Exact hostname→bucket mappings, |-separated: host:bucket|host2:bucket2 |
HOST_PATTERNS |
— | Regex patterns with named groups, |-separated: pattern:template|... |
CACHE_MAX_MB |
128 |
Total cache size cap in MB |
CACHE_TTL_SECS |
60 |
Cache entry TTL |
CACHE_MAX_FILE_KB |
512 |
Files larger than this are streamed, not cached |
S3_TIMEOUT_SECS |
15 |
Per-request timeout for S3 fetches |
CACHE_CONTROL |
— | Value for Cache-Control response header (e.g. public, max-age=300) |
METRICS_LISTEN |
— | Bind address for the Prometheus /metrics endpoint (e.g. 0.0.0.0:9090). Disabled if unset. |
RUST_LOG |
— | Log level (info, debug, etc.) |
Host mapping examples
Exact:
HOSTS=jmarya.me:jmarya-me|docs.example.com:my-docs-bucket
Regex with named capture groups (bucket name extracted from hostname):
HOST_PATTERNS=s3pub-(?P<bucket>[^.]+)\.hydrar\.de:{bucket}
Both are checked in order — exact matches first, then patterns top to bottom.
Path resolution
For a request to /some/page:
| Path type | Candidates tried (in order) |
|---|---|
/ or trailing slash |
index.html |
Has extension (.jpg, .html, …) |
exact path |
| Extensionless | some/page.html → some/page/index.html → some/page |
On total miss, serves 404.html from the bucket root with a 404 status. If that's also missing, returns a plain-text 404.
Building
# Binary
nix build
# Container image (loads directly into Docker)
nix build .#dockerImage && docker load < result
Running
ENDPOINT=https://s3.example.com \
HOSTS=jmarya.me:jmarya-me \
HOST_PATTERNS='s3pub-(?P<bucket>[^.]+)\.hydrar\.de:{bucket}' \
RUST_LOG=info \
./sh3rine
Response headers
| Header | Values | Meaning |
|---|---|---|
x-sh3rine-cache |
HIT / MISS / STREAM |
Whether the response came from cache |
Observability
Logs
Set RUST_LOG=info for one structured log line per request:
host=jmarya.me bucket=jmarya-me path=/blog/post status=200 cache=HIT duration_ms=1
Set RUST_LOG=debug to also see individual S3 key candidates being tried, cache operations, and streaming decisions.
Prometheus metrics
Set METRICS_LISTEN=0.0.0.0:9090 to enable the /metrics endpoint. Keep this port off the public-facing listener — configure your ingress or firewall accordingly.
| Metric | Type | Labels | Description |
|---|---|---|---|
sh3rine_requests_total |
Counter | bucket, status, cache |
All requests handled |
sh3rine_request_duration_seconds |
Histogram | bucket, cache |
End-to-end request latency |
sh3rine_upstream_requests_total |
Counter | bucket, result |
S3 fetches (hit, miss, timeout, connect_error, server_error, read_error) |
sh3rine_upstream_duration_seconds |
Histogram | bucket |
S3 fetch latency (excludes cache hits) |
sh3rine_cache_entries |
Gauge | — | Current number of cached entries |
sh3rine_cache_size_bytes |
Gauge | — | Current weighted cache size in bytes |
sh3rine_cache_max_bytes |
Gauge | — | Configured cache cap in bytes |
Grafana
A ready-to-import dashboard is at grafana/dashboard.json. Import it via Dashboards → Import → Upload JSON file and point it at your Prometheus datasource.