Open-source project
losfair/zeroserve avatar
losfair/zeroserve

zeroserve: a single tarball web server with eBPF request scripting

Zero-config, fast `io_uring`-based HTTPS server.

626 stars8 forksRustMIT

At a glance

What is it?
zeroserve is a Rust HTTPS server built on io_uring that serves a site packaged as one tarball, runs request scripts written in C or Rust and JIT-compiled to eBPF, and adapts a Caddyfile into a script. It is MIT licensed, but it is still an alpha release on a 0.2 line and depends on a patched monoio fork.
Who is it for?
zeroserve fits a Linux operator who wants a single static binary that serves a small site, adds rate limiting, reverse proxying or OIDC login without writing a service, and who is comfortable with eBPF and with a young project. It does not fit a team that needs a battle-tested server, a non-Linux target, or a scripting model that does not compile to eBPF, since the release line is still v0.2.12 alpha and the runtime depends on a forked monoio.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 37 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What zeroserve does differently from a static file server

zeroserve is described as a zero-config, fast, scriptable io_uring HTTPS server. The unusual part is the second half of that phrase. Instead of a configuration file describing routes, you write small C or Rust programs that run per request, and those programs are compiled to eBPF and JIT-executed.

The site itself is a single tarball. It is indexed at load time into a path to byte range mapping and then served directly through byte range reads, with no extraction and no temporary files on disk. On top of that sits the scripting layer, which can inspect and rewrite requests, generate responses, reverse-proxy, rate-limit, do crypto, sign AWS SigV4 requests, and put the whole site behind an OAuth2 or OIDC login, all from small scripts.

There is a Caddy compatibility layer as well. A Caddyfile or Caddy JSON config can be adapted into a zeroserve script and served in one command, with the details in a CADDY_COMPAT.md file in the repository. That is the shortest path for someone who already has a Caddy configuration and does not want to rewrite it.

The users are therefore people running a small service on Linux who want programmable request handling without running a reverse proxy, a scripting language runtime, and a web framework in production. The trade is that the programming model is unusual: your request logic is an eBPF program, not a function in a managed runtime.

The io_uring and monoio stack underneath

All network and disk I/O runs on the monoio runtime, with a per-core worker thread and per-worker listeners. Where the kernel supports it, the load-balancing socket options SO_REUSEPORT and SO_REUSEPORT_LB are used so the kernel balances accepts. Elsewhere a shared listening socket is used, with accepted connections handed off round-robin so keep-alive traffic stays balanced across workers.

That fallback detail matters more than it looks. On kernels without the load-balancing option, every worker competes for the same accept queue, and a single busy connection can skew the distribution. The round-robin handoff is the answer to a problem the kernel does not solve for you.

The dependency is a fork. The Cargo.toml pins monoio and monoio-compat to a git revision of the author's own fork of the project, with a comment explaining that the fork adds OpenBSD support for the legacy driver, and a separate target section for Linux that unions the base dependency so Linux also gets io_uring. Other dependencies in the same file show the shape of the project: BoringSSL as the TLS stack, clap for the command line, async-ebpf for the scripting layer, io-uring and caps for Linux specifics, and nix for namespace and filesystem work.

The edition is 2024 and the release profile sets panic = "abort". The build script uses cc, lalrpop and sha2, so a grammar and a hashing step are part of the compile.

Packing a directory and serving it in two commands

The quick start is two commands. The first packs a directory into a site tarball:

bash
# Pack the current directory into a site tarball
zeroserve --pack ./public > site.tar

The second serves it:

bash
# Serve it over HTTP on :8080
zeroserve --addr 0.0.0.0:8080 site.tar

The default address is 0.0.0.0:8080, so the simplest invocation of the binary with a tarball argument is enough to serve over HTTP.

What --pack does is worth knowing. It walks the directory, adds every regular file, and compiles any .zeroserve/scripts/*.c request script to an eBPF object file, omitting the C source from the tarball. So the scripts are compiled at pack time, not at request time, and the shipped artefact contains only the compiled form.

The scripts live under .zeroserve/scripts/ in the site directory and run in filename order. The SDK header is at sdk/zeroserve.h, and the same header can be dumped from the binary with zeroserve --dump-sdk.

For a longer reference, zeroserve --manual prints the full embedded user manual, which also lives in docs/user_manual.md, and zeroserve --help gives the complete flag reference.

Writing a request script in C

A request script is a C file that includes the SDK header and defines an entry point. The example in the documentation handles a health check:

c
#include <zeroserve.h>

ZS_ENTRY
zs_u64 entry(void) {
  char path[64];
  zs_req_path(path, sizeof(path));

  if (zs_strcmp(path, "/health") == 0) {
    zs_meta_set(ZS_STR("zs.response.header.content-type"), ZS_STR("application/json"));
    zs_respond(200, ZS_STR("{\"status\":\"ok\"}\n"));
  }
  return 0;
}

The shape is a single entry function with a fixed signature, marked by the ZS_ENTRY macro, returning a u64. The helper API covers request and response inspection and mutation, response templating, logging and time, crypto and encoding with SHA-256, HMAC, base64, hex and random, AWS SigV4 signing, rate limiting, reverse proxy, OAuth2 and OIDC login with sealed cookies, and strongSwan VICI lookups.

Scripts are compiled to eBPF and JIT-executed per request inside a sandbox that is bounded in both memory and time. The bounding is what makes this a server rather than a footgun: a script that loops or allocates without limit is contained.

The project prefers precompiled eBPF object files, but it also accepts raw C files, because zeroserve embeds a tinycc with an eBPF backend patch that compiles C to eBPF on the fly. The examples/ directory in the tree holds runnable scripts, including health_response.c, log_request.c, template.c, reverse_proxy.c, acme.c, connection_info.c, call_gateway.c, call_greeter.c and infinite_loop.c, and that last one is a good name for the boundary case the sandbox exists to contain.

TLS, SNI selection and encrypted client hello

TLS is handled by BoringSSL, and the server supports TLS 1.3. Certificates can be given as a file pair, or selected by SNI from a directory of PEMs:

bash
# HTTPS with SNI certificate selection from a directory of PEMs
zeroserve --tls-addr 0.0.0.0:8443 --cert-dir /etc/zeroserve/certs site.tar

Encrypted Client Hello is supported as well, with key rotation and a transparent relay fallback when a client does not support ECH. That fallback is what makes ECH deployable in practice, since a server that simply refuses non-ECH clients loses half its traffic.

Host validation is a separate switch. A server can reject requests whose Host or SNI header is not in an allow list, and otherwise returns 421:

bash
# Reject requests whose Host/SNI is not in the allow-list (otherwise 421)
zeroserve --validate-hostnames example.com,www.example.com site.tar

The other listener options cover the deployment shapes people actually have. There is a flag to fall back to a path.html file when a request path is missing, flags to honour PROXY protocol v1 on both listeners or on just one of them, and an address form that inherits a pre-bound socket for socket activation, written as a file descriptor number.

Hardening, hot reload and the runtime isolation model

The server hardens itself with Linux namespaces and capability dropping, and it exposes an explicit --expose-filesystem opt-in for any host filesystem access. The default is therefore a closed world, and opening it is a decision you have to make on the command line.

Hot reload is a signal. Sending SIGHUP reloads the tarball, the certificates and the scripts in place, and the documentation gives the blunt one-liner:

bash
# Hot-reload tarball, certificates, and scripts in place
killall -SIGHUP zeroserve

Because the site is indexed in memory as a path to byte range map and served without extraction, a reload is a metadata swap rather than a file re-read, which is what makes it usable for something as frequent as a certificate renewal.

Scripts can also run standalone with no static files at all, for instance zeroserve auth.c, and plugins can be scheduled ahead of the site's own scripts with --plugin and --plugin-dir.

The distributed images are signed by digest with keyless Sigstore certificates, and every release includes SHA256SUMS and a keyless Sigstore bundle for each binary, with Cosign and the GitHub CLI attestation flow documented for verification.

Where zeroserve is the wrong tool

The first limit is the maturity of the release line. The most recent tags are v0.2.12-alpha.16, v0.2.12-alpha.14 and v0.2.12-alpha.12, so this is an alpha with an alpha suffix in the version itself, and the package version in Cargo.toml is 0.2.12-alpha.16. The last push was on 2026-09-16, roughly a month before now, so the code is being worked on, but an alpha is an alpha.

The second is the platform. The build instructions for source say Linux only, and require the pinned Rust toolchain, Zig 0.14.0 and cargo-zigbuild 0.22.3. The io_uring story, the namespaces, the capability dropping and the eBPF execution are all Linux specific. There is no macOS or Windows path described.

The third is the programming model. If your request logic is anything other than a small C or Rust program that compiles to eBPF, this is a poor fit. There is no general purpose scripting language, and while the Caddy compatibility path exists, it is a translation into a zeroserve script, so a Caddyfile with plugins you rely on will not survive intact. Reading CADDY_COMPAT.md before you migrate is the way to find out what you lose.

The fourth is that a fork is in the dependency graph. monoio is pinned to a git revision of a personal fork rather than a published crate release, which means your build depends on that fork's availability and on the patch continuing to apply.

Compare that with running Caddy itself, or nginx with a Lua module. Those give you a mature request pipeline, a large plugin ecosystem and a configuration language designed for routing, and they give up the eBPF scripting model and the single tarball packaging. The difference in approach is that zeroserve pushes programmability down into the kernel's execution model to keep the serving path minimal, and that trade only pays off if you actually write scripts.

Licence, reproducible builds and the cost of a source install

zeroserve is released under the MIT licence, with the text in the LICENSE file at the repository root. The MIT terms permit use, modification and redistribution in closed and open source products, provided the copyright notice and licence text travel with copies. That is a short obligation compared with the copyleft licences that many comparable infrastructure projects use, which makes embedding it in a commercial image straightforward. Read the LICENSE file yourself for the exact wording.

The release engineering is the most interesting part of the project for a reviewer. The release workflow builds each Linux target twice from independent source and target directories and requires byte-for-byte identical results before signing or publishing. The normal development build is still cargo build --release --locked, and the reproducible build is driven by a script:

bash
cargo install cargo-zigbuild --version 0.22.3 --locked
./tools/reproducible-build.sh x86_64-unknown-linux-gnu.2.31 \
  target/zeroserve-x86_64-linux

The build needs Zig and cargo-zigbuild on top of the Rust toolchain, which is a heavier setup than a plain cargo build, and the script pins a glibc baseline of 2.31 for the target.

The Dockerfile in the tree shows a straightforward two-stage build on Rust 1.97.1 slim trixie, producing a debian trixie slim image that exposes ports 8080 and 8443 and sets the binary as the entrypoint, with WORKDIR at /srv. The published images are multi-arch, covering amd64 and arm64.

Editorial conclusion

zeroserve fits a Linux operator who wants a single static binary that serves a small site, adds rate limiting, reverse proxying or OIDC login without writing a service, and who is comfortable with eBPF and with a young project. It does not fit a team that needs a battle-tested server, a non-Linux target, or a scripting model that does not compile to eBPF, since the release line is still v0.2.12 alpha and the runtime depends on a forked monoio. Before adopting it, read the CADDY_COMPAT.md document to see how much of a Caddyfile survives the translation, test your scripts against the examples/ directory, and verify a downloaded release with the SHA256SUMS and Sigstore bundle the project publishes.

Frequently asked questions

How do I serve a site with zeroserve?

Pack a directory with zeroserve --pack ./public > site.tar, then run zeroserve --addr 0.0.0.0:8080 site.tar. The tarball is indexed at load time and served through byte range reads, with no extraction and no temporary files on disk.

What language are zeroserve request scripts written in?

They are C files using the SDK header at sdk/zeroserve.h, and Rust is also an option. Scripts are compiled to eBPF and JIT-executed per request inside a memory and time bounded sandbox, and raw C files are compiled on the fly by an embedded tinycc with an eBPF backend patch.

Can zeroserve run behind a load balancer?

Yes. There are flags to honour PROXY protocol v1 on both listeners or only on the listener behind the balancer, and the server can also inherit a pre-bound socket for socket activation with an address in the form fd:3.

Is zeroserve production ready?

The release tags are alpha versions, with v0.2.12-alpha.16 the most recent, and the package version in Cargo.toml is 0.2.12-alpha.16. The last push was on 2026-09-16, so the code is current but the release line is still alpha.

How do I verify a zeroserve release?

Every release ships SHA256SUMS and a keyless Sigstore bundle per binary. The documented flow checks the checksum with sha256sum, then verifies the blob with cosign using a certificate identity tied to the release workflow, and optionally uses gh attestation verify.

What licence does zeroserve use and can I use it commercially?

zeroserve is released under the MIT licence, with the terms in the LICENSE file at the repository root. The MIT terms allow commercial use and redistribution provided the copyright notice and licence text accompany copies.

Official sources

  1. License: MIT
  2. losfair/zeroserve on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/losfair-zeroserve.svg)](https://hysenlabs.com/projects/losfair-zeroserve)

Was this useful?