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.2.tar.gz (198.9 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.2-py3-none-manylinux_2_28_aarch64.whl (7.5 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

tenement-0.2.2-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.2-py3-none-macosx_11_0_arm64.whl (7.4 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

tenement-0.2.2-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.2.tar.gz.

File metadata

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

File hashes

Hashes for tenement-0.2.2.tar.gz
Algorithm Hash digest
SHA256 e647049dfa74b8c9bbe4f14ee1bb262ca7801d8be13bf60bc3232ac3f2da108c
MD5 bb3c41b0269bb7632ade8785dfb0ce5f
BLAKE2b-256 b9b5e2cd61ec2d68ebdf08f240066ddfa3b0724fa637e9ac16098be5ad0d9125

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.2.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.2-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for tenement-0.2.2-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 17e3fb3b3b7108478da2214c5776067e4058f868f420da1f92f84ae28cc7b9c8
MD5 1e34617a29f0b24337a9e1357678e2d7
BLAKE2b-256 a87d3cb2e2d1c1e53bcf9e53efb288d9d7aedbe895a7f1922324b43e46d0a32e

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.2-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.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for tenement-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 d7360fb68433c1f05ae928cb95dc2e14bae73e8c5e637507eb4870c9dec1b5a2
MD5 5c84999a72df48b69f8cd960977e7d96
BLAKE2b-256 1b8913c67945b8d41aaab09d385b56bc22f392c1494edd74d37bad060667c417

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.2-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.2-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for tenement-0.2.2-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 37c59ed016f32efbb4aee73a931e8bfb64e69b4474fa8fa2a0ca3ffce28d6f67
MD5 7955a1270c4d35b6fc6b00b31fc624b0
BLAKE2b-256 41fd0f7b8301694698c73b3544ae071a8a746601ed8946a76d2d76e55fc863a2

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.2-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.2-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for tenement-0.2.2-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 f457341af1ae0d72dbf610d9e79fa730c156979e1558f87756938ed0a3ca2eb5
MD5 87b6c31dce9407de53f3d73f4e66f7d2
BLAKE2b-256 5a4df2c4916017e4f05d5b54464dcfb94125a3593b8b977a221e46e4ca9cc1fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for tenement-0.2.2-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