Skip to main content

Hyperlightweight process hypervisor for single-server deployments

Project description

tenement

Lightweight Rust hypervisor for single-server deployments of many single-tenant processes.


tenement is a process hypervisor for running multi-tenant services on a single server. It spawns one process per tenant, routes requests by subdomain, runs HTTP health checks, and stops idle instances automatically. When the next request arrives, it wakes them back up in under a second.

You write your app as if it serves one customer. tenement runs a copy for each of them.

alice.notes.example.com  ->  notes:alice  ->  isolated process + own database
bob.notes.example.com    ->  notes:bob    ->  isolated process + own database

Experimental. Actively developed. APIs may change.

Why this exists

systemd can run processes, but it doesn't route requests or stop idle ones. You'd write a unit file for each customer and wire up nginx yourself. Docker adds container overhead you don't need for trusted code on one machine. Kubernetes is absurd for a $5 VPS.

tenement is Fly Machines on your own hardware. Spawn a process, give it a subdomain, let it sleep when nobody's using it, wake it up on the next request.

systemd tenement
Routing You configure nginx per service alice.notes.example.com just works
Scale to zero Processes run forever Idle processes stop, wake on first request
Per-tenant data You manage it Each instance gets its own data directory
New customer Write a unit file, reload ten spawn notes:alice
Health + restart Basic restart-on-failure HTTP health checks, exponential backoff
Deployment Rolling restart scripts ten deploy notes:v2 then ten route --from v1 --to v2
Logs journalctl ten logs notes:alice with full-text search

Quick start

Install the CLI and start the server:

cargo install tenement-cli
ten serve --port 8080 --domain localhost
ten token-gen

Here's a complete app. It's a notes API backed by SQLite, and it doesn't know anything about tenants. It just reads PORT from the environment and serves whoever's asking.

# app.py
import os, json, sqlite3
from http.server import HTTPServer, BaseHTTPRequestHandler

PORT = int(os.environ["PORT"])
DB = os.path.join(os.environ.get("DATA_DIR", "."), "notes.db")

def get_db():
    os.makedirs(os.path.dirname(DB) or ".", exist_ok=True)
    db = sqlite3.connect(DB)
    db.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)")
    return db

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/health":
            self.respond(200, {"status": "ok"})
        else:
            db = get_db()
            notes = [{"id": r[0], "text": r[1]} for r in db.execute("SELECT * FROM notes").fetchall()]
            db.close()
            self.respond(200, notes)

    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
        db = get_db()
        db.execute("INSERT INTO notes (text) VALUES (?)", (body["text"],))
        db.commit()
        db.close()
        self.respond(201, {"ok": True})

    def respond(self, code, data):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()

The config is six lines. You tell tenement what command to run, where the health endpoint is, and what environment variables to set. The {id} in DATA_DIR gets replaced with the tenant name at spawn time.

# tenement.toml
[service.notes]
command = "python3 app.py"
health = "/health"
idle_timeout = 300
isolation = "process"

[service.notes.env]
DATA_DIR = "{data_dir}/{id}"

Now spawn a couple tenants and try it:

ten spawn notes:alice
ten spawn notes:bob

curl -X POST http://alice.notes.localhost:8080/notes \
  -H "Content-Type: application/json" -d '{"text":"hello from alice"}'

curl http://alice.notes.localhost:8080/notes   # alice's notes
curl http://bob.notes.localhost:8080/notes     # bob's notes (empty, different database)
ten ps                                         # list running instances

Alice and Bob each get their own process, their own SQLite database, their own data directory. After 5 minutes of no requests, tenement kills the process. The next request wakes it back up:

Runtime Cold wake (median)
Python ~65ms
Node.js ~105ms
Go (go run) ~140ms

How it works

You define a service in your config (the command, health endpoint, and environment variables). When you spawn an instance, tenement allocates a TCP port, sets PORT in the environment, and starts the process. Requests to alice.notes.example.com get proxied to alice's port. tenement polls the health endpoint and restarts unhealthy instances with exponential backoff. When nobody's made a request for a while, it kills the process. When someone does, it spawns a fresh one.

Your app handles its own auth, business logic, and data. tenement handles routing, lifecycle, and isolation. These two layers are completely independent, which means tenement doesn't touch your Authorization headers or care what framework you're using. You can verify this yourself with the auth-test example.

The economics

Most SaaS customers aren't active simultaneously. If you have 1000 customers and only 20 are using the product at any given moment, the traditional approach keeps all 1000 processes running. That's 20GB of RAM across 10 machines at maybe $500/month. With tenement, the 980 idle instances cost nothing. You run 20 processes on one machine for $5/month. The wake-on-request latency is under a second, so users don't notice.

This pairs well with SQLite. Each customer gets their own database file, replicated to S3 with something like walrust or Litestream. No shared Postgres, no connection pooling, no schema migrations that touch everyone's data at once.

What's in the box

Tenement does subdomain routing (alice.api.example.com routes to api:alice), scale-to-zero with wake-on-request, per-tenant data directories, process isolation via Linux namespaces, HTTP health checks with exponential backoff, weighted routing for blue-green and canary deployments, built-in TLS via Let's Encrypt, Prometheus metrics, log capture with full-text search, and a bearer token auth system for the management API with both admin and tenant-scoped tokens.

Commands like uv run python app.py or go run main.go are shell-split automatically, and every instance runs in its own process group so killing it also kills any child processes. No orphans.

CLI

ten serve --port 8080 --domain localhost    # start the server
ten spawn notes:alice                       # create a tenant
ten stop notes:alice                        # stop a tenant
ten ps                                      # list everything
ten logs notes:alice                        # tail logs
ten logs -f                                 # follow all logs
ten deploy notes:v2                         # deploy a new version
ten route notes --from v1 --to v2           # blue-green swap
ten weight notes:alice 50                   # canary: 50% traffic
ten token-gen                               # admin API token
ten token-gen --tenant alice                # scoped token for alice

Set TENEMENT_SERVER to skip passing --server on every command:

export TENEMENT_SERVER=http://localhost:9090
ten ps    # just works

Configuration

[settings]
data_dir = "./data"

[service.api]
command = "uv run python app.py"
health = "/health"
isolation = "process"            # "process" (macOS/Linux) or "namespace" (Linux, PID isolation)
idle_timeout = 300               # stop after 5 min idle
startup_timeout = 10             # increase for go run (30s)
storage_persist = true           # keep data across restarts
memory_limit_mb = 256            # cgroups limit (Linux)

[service.api.env]
DATA_DIR = "{data_dir}/{id}"     # {name}, {id}, {data_dir}, {port} all interpolate

Full reference at tenement.dev/guides/03-configuration.

Examples

The examples/ directory has complete working setups you can run immediately:

  • hello-world is the simplest possible setup, a bash script and netcat.
  • python-fastapi and node-fastify and go-http show the same pattern in three languages.
  • multi-runtime runs all three at once with a 56-test integration script that verifies auth, data isolation, and cross-service isolation.
  • auth-test demonstrates that tenement passes all request headers through untouched, so your app's auth works exactly as it would without tenement.
  • multi-tenant is a per-tenant notes API with SQLite, which is probably closest to what you'd actually build.

Production

For a Hetzner or DigitalOcean VPS with wildcard HTTPS:

# Point *.app.example.com at your server IP, then:
cargo install tenement-cli
cd /opt/myapp
ten init --name myapp --command "python3 app.py"
ten token-gen
ten install --domain app.example.com --caddy --dns-provider cloudflare

ten spawn myapp:customer1
ten spawn myapp:customer2
ten ps

The ten install command creates a systemd service and a Caddyfile with wildcard TLS. Caddy handles HTTPS, tenement handles everything else.

Development

cargo test    # 566 tests
cargo bench

Full docs at tenement.dev. See ROADMAP.md for what's next.

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tenement-0.2.1.tar.gz (195.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

tenement-0.2.1-py3-none-manylinux_2_28_aarch64.whl (7.5 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

tenement-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (8.1 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

tenement-0.2.1-py3-none-macosx_11_0_arm64.whl (7.4 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

tenement-0.2.1-py3-none-macosx_10_12_x86_64.whl (7.8 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file tenement-0.2.1.tar.gz.

File metadata

  • Download URL: tenement-0.2.1.tar.gz
  • Upload date:
  • Size: 195.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for tenement-0.2.1.tar.gz
Algorithm Hash digest
SHA256 72c44113ccdd9d84d87b8a81b07f471960c5fd134710283a198649734a2ac8a8
MD5 7bea2345f197704d64f3bc1cf57b6e2c
BLAKE2b-256 022c8515256bfaab4215a3f5046ef86f1c07b25d81bb20bbd7f248677fd863c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.1.tar.gz:

Publisher: release.yml on russellromney/tenement

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tenement-0.2.1-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for tenement-0.2.1-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 be012bb778793cbd8922b855c0f4f1d539c58a7da0c04460fcd15ac0bbc0c40e
MD5 4643190bbf00f43e7f18db5001cb16f9
BLAKE2b-256 2917300facaede89712ed5fe875b08c47df1928171b64c8048939d09eeecd6e9

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.1-py3-none-manylinux_2_28_aarch64.whl:

Publisher: release.yml on russellromney/tenement

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tenement-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for tenement-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 de69d12464cb478edaa748a5360192b4bd64dad1e592aaca035aaac2198d19b6
MD5 aac16e8513657d97fa7ef335c9ec0b96
BLAKE2b-256 7e6885b311b91f85fd600430f61b0a1014726c4cedba3b4bf45fcf6b293e1c14

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on russellromney/tenement

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tenement-0.2.1-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for tenement-0.2.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a6b39272f4395c2f6cbbd0829ef430958ac518666069f9421375e5a11ff68978
MD5 e46e50dbde1ec2114e445df0df26af68
BLAKE2b-256 3228279a955b515e53428cf679bd25d244a581917e1363cb0a33bc8b9b155af1

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.1-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on russellromney/tenement

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tenement-0.2.1-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for tenement-0.2.1-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 c3da20c6f5d097427c8b576359b11fb9b0c2d169834899360b3d9c16d4b42379
MD5 ec5c4dd451f5abdb5c460ad7d126737d
BLAKE2b-256 a25ec3bc9a7662aa41cffa6ba482b51e2fd981d0471f03d88416c1c34a033484

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.1-py3-none-macosx_10_12_x86_64.whl:

Publisher: release.yml on russellromney/tenement

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page