Multi-Tenant WaaS
One host → many isolated dev rooms.
Each tenant gets: own container (ghost-tenant-<id>), own SSH port (auto from 2225), own mounts/tenants/<id> folder, CPU/RAM quotas via cgroup, and a registry entry in config/tenants.json.
Ideal for classrooms, VPS hosts, and team WaaS.
Architecture & Isolation
| Guarantee | How |
| Storage isolation | Non-overlapping bind mounts: mounts/tenants/<id> per tenant. No cross-tenant access. No shared volume leaking. |
| Resource quotas | cgroup cpus: 2.0 + memory: 4G + optional pids_limit per tenant — noisy neighbor is throttled. |
| Network identity | Bridge ghost_sandbox (external: true), one mapped port per tenant (2225 → tenant alice, 2226 → bob). Optionally Cloudflare Tunnel per tenant. |
| Lifecycle | Per-tenant docker compose -p ghost-tenant-<id> — start/stop/snapshot/delete one without touching others. |
A. Prebuilt (fast — preferred)
Pull one GHCR package per engine, then add. The script honors GHOST_IMAGE/TENANT_IMAGE if set — it reuses the pulled image instead of rebuilding.
# Debian for alice, Ubuntu for bob, Alpine for carol, Arch for dave
docker pull ghcr.io/1999azzar/ghost-machine-debian:latest
./tenant.sh add alice --engine debian --cpu 2 --mem 4G --port 2225
docker pull ghcr.io/1999azzar/ghost-machine-ubuntu:latest
./tenant.sh add bob --engine ubuntu --cpu 4 --mem 8G # auto-port 2226
docker pull ghcr.io/1999azzar/ghost-machine-alpine:latest
./tenant.sh add carol --engine alpine --port 2227
docker pull ghcr.io/1999azzar/ghost-machine-arch:latest
./tenant.sh add dave --engine arch --port 2228
B. Build Locally (when you need local changes)
# forces docker compose build from Dockerfile.*
./tenant.sh add alice --engine debian --build
./tenant.sh add bob --engine arch --build --cpu 4 --mem 8G
Manage — Daily Ops
./tenant.sh list # TENANT ID | ENGINE | PORT | LIMITS | STATUS | STORAGE SIZE
./tenant.sh stats # live docker stats for all running tenants
./tenant.sh stats alice
./tenant.sh exec alice "cargo test"
./tenant.sh start alice # restart a stopped tenant
./tenant.sh stop alice
./tenant.sh restart alice
./tenant.sh delete alice -y # skips confirm; add --archive to save mount first
Snapshots — Per-Tenant
Each tenant's mount can be archived with SHA-256 + .meta.json (no cache bloat — node_modules/.cache, target/ excluded).
./tenant.sh snapshot alice # → snapshots/snapshot_alice_20260901_...tar.gz + .sha256 + .meta.json
./tenant.sh restore alice snapshots/snapshot_alice_20260901.tar.gz
./tenant.sh snapshot alice --archive # (via delete --archive)
Per-Tenant Flags (full)
./tenant.sh add <id>
--engine <ubuntu|debian|alpine|arch> # default: debian
--port <port> # default: auto ≥ 2225 (find_next_port)
--cpu <cores> # default: 2.0 (e.g. 4.0 for 4 vCPU)
--mem <size> # default: 4G (e.g. 8G)
--user <name> # default: developer
--pubkey <path> # SSH key path; else auto-detects ~/.ssh/id_ed25519.pub
--build # force local build instead of GHCR
-y # skip confirm prompts
Storage path is always mounts/tenants/<id> on host — even after delete, work is only removed if you confirm.
When vs Single Mode
Single (./start.sh) = one workstation for you.
Tenant = vend many workstations to others on the same host.
Both can coexist: run ghost-machine-single for you while vendoring ghost-tenant-alice/bob to others — they use separate Compose projects and ports.