Orva is in active development and the maintainers don't recommend it for production-critical workloads yet. The notes below are for operators running it in homelabs, side projects, internal tools, staging environments — places where a few hours of downtime is acceptable while bugs get sorted out.
Measured on a 2-CPU / 12 GB host (see CAPACITY.md):
- Idle: ~50 MB RSS for the orvad process. Each warm worker takes ~18 MB when idle. 20 deployed-but-unused functions cost ~50 MB total.
- Under sustained c=500: ~880 req/s aggregate, 35% returning fast
429 TOO_MANY_REQUESTSonce the host-wide sandbox concurrency cap is hit. p50 of successful invocations: ~500 ms.
For a single-host deploy, 2 CPU + 4 GB RAM is plenty for ~50 functions of mixed traffic. Bigger if your functions hold significant memory each.
The shipped docker-compose.yml uses Docker's default container
runtime (runc) — the right choice for homelab and trusted-code
use. To run Orva under a stricter, hypervisor-class runtime (Kata
Containers with Cloud Hypervisor underneath), add one line to the
orva service in your local docker-compose.yml:
services:
orva:
runtime: kata-clh # or: kata (QEMU hypervisor)
# ...rest unchangedThen recreate:
docker compose down && docker compose up -d
docker inspect orva --format 'runtime={{.HostConfig.Runtime}}'
# → runtime=kata-clhPrerequisites (Kata installed + the runtime registered in
/etc/docker/daemon.json) and the measured perf cost (~75–80 %
throughput tax, ~14 s slower container-start for QEMU vs CLH) are in
docs/KATA.md. Kata-CLH is the recommended Kata path when
you want hypervisor-class isolation around Orva.
orvad does not terminate TLS itself. Run a reverse proxy in front: caddy, nginx, traefik, cloudflared. The browser's clipboard API silently fails on plain HTTP from non-localhost — the dashboard becomes partially broken.
If your proxy forwards X-Forwarded-Proto: https (the nginx example below does), Orva sets the Secure flag on session cookies by itself. Set ORVA_SECURE_COOKIES=true only when it cannot see the real scheme so
session cookies are only sent over HTTPS.
Caddy example:
orva.example.com {
reverse_proxy localhost:8443 {
flush_interval -1 # required so SSE streams aren't buffered
}
}nginx example:
server {
listen 443 ssl http2;
server_name orva.example.com;
ssl_certificate /etc/letsencrypt/live/orva.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/orva.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8443;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 600s;
chunked_transfer_encoding off;
}
}Critical bits: buffering off for SSE streams (/api/v1/events and
/api/v1/deployments/*/stream).
Everything Orva persists lives under /var/lib/orva/. Two strategies:
Stop orvad → snapshot/copy the data dir → restart.
systemctl stop orva
tar -czf orva-backup-$(date +%F).tar.gz /var/lib/orva
systemctl start orvaA few seconds of downtime per backup. Restore by extracting back into
/var/lib/orva/ before systemctl start.
The DB is in WAL mode — safe to back up while orvad is running, as long as you use SQLite's online backup API (not a raw file copy).
sqlite3 /var/lib/orva/orva.db ".backup /backup/orva-$(date +%F).db"This grabs a consistent snapshot of just the database. The version
archive at /var/lib/orva/functions/<id>/versions/ and the rootfs
trees at /var/lib/orva/rootfs/ need a separate tar while orvad is
either stopped or guaranteed not to be deploying / GC'ing.
| path | importance | grows |
|---|---|---|
/var/lib/orva/orva.db (+ -wal, -shm) |
critical — users, functions, secrets, executions | slowly |
/var/lib/orva/.admin-key |
recovery key — the only persisted plaintext copy | static |
/var/lib/orva/.master.key |
critical — the AES-256-GCM key every function secret is encrypted with | static |
/var/lib/orva/functions/*/versions/<hash>/ |
rollback targets | bounded by versions_to_keep |
/var/lib/orva/functions/*/current (symlink) |
active version pointer | static |
/var/lib/orva/rootfs/ |
~600 MB of language base images | static; re-seeded by the Docker entrypoint on an empty volume, or by install.sh on bare metal |
A backup of the database and version archives alone is not sufficient.
Secrets are stored encrypted; .master.key is the only copy of the key that
decrypts them, and it lives beside the database rather than inside it. Restore
without it and every secret is unrecoverable ciphertext — the functions come
back and then fail at runtime on a missing credential. Back up .master.key
and .admin-key together with orva.db, and treat them with the same care as
the database itself.
The rootfs trees are the one thing you can safely omit: the Docker entrypoint
copies them out of the image when the volume is empty, and scripts/install.sh
downloads them for a bare-metal install. Neither rebuilds them on an existing
install that has lost them — re-run the installer, or recreate the container.
Orvad writes to stdout. Three sources:
- HTTP request logs — one line per request via
slog, always JSON. The format is fixed; there is no knob to change it. - Function execution logs — stored in SQLite (
execution_logstable), not on disk. - Build logs — same, in
build_logs.
Execution history is pruned automatically: system_config.execution_retention_days
(default 30; 0 disables it) is first applied one hour after startup, then every 24 hours, removing
old executions rows together with their logs, captured requests, structured log
entries and user spans. See CONFIG.md.
Build logs are covered: the same sweep removes build_logs for terminal
deployments past the cutoff, along with finished jobs, webhook_deliveries
and expired sessions. None of that needed a cron job before this was true,
and none needs one now.
If you are catching up an instance that ran for a long time before this existed, a one-off compaction reclaims the space the sweep has since freed:
docker exec orva sqlite3 /var/lib/orva/orva.db "VACUUM;"Prefer POST /api/v1/system/vacuum (or the dashboard's Compact database
button) over raw sqlite3 — it checkpoints the WAL first and reports a busy
checkpoint instead of silently compacting a stale file.
Docker stdout logs rotate via the daemon's log-driver config (the
shipped docker-compose.yml sets max-size: 10m, max-file: 5).
# With the shipped compose (recommended — it carries all the sandbox flags):
docker compose pull && docker compose up -d
# Or with plain docker run, re-passing the full flag set and the same volume:
docker pull ghcr.io/harsh-2002/orva:latest
docker stop orva && docker rm orva
docker run -d --name orva -p 8443:8443 \
--pid host --cgroupns host \
--cap-add SYS_ADMIN \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
--security-opt systempaths=unconfined \
--device /dev/net/tun \
-v orva-data:/var/lib/orva \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
ghcr.io/harsh-2002/orva:latest--pid host + --cgroupns host are required on the runc runtime: nsjail
enrolls each sandbox PID in the host cgroup hierarchy, and omitting them
makes every invocation fail with Launching child process failed.
The DB schema migrations are idempotent additive ALTERs, so running a newer
image on an older volume is safe — with one carve-out. The UUIDv7 id
migration rewrites every storage id and renames <dataDir>/functions/<id>/
to match, in a boot step that is fatal on failure by design. It is idempotent
and resumable, and your code is never at risk, but it is one-way: back up
before upgrading across it. See Upgrading across the UUIDv7 id
migration.
Downgrade is not safe.
sudo systemctl stop orva
curl -fsSL https://github.com/Harsh-2002/Orva/releases/latest/download/install.sh | sudo sh
sudo systemctl start orvaThe installer is idempotent — same data dir is preserved. Re-running it also
rewrites the service unit, which is how a unit predating
RestartForceExitStatus=70 picks that up. Without it, orva backup restore
exits 70 to force a restart and systemd's Restart=on-failure leaves the
service down instead. Verify with
systemctl cat orva | grep RestartForceExitStatus. On Alpine/OpenRC the same
restore needs supervisor="supervise-daemon" in /etc/init.d/orva
(start-stop-daemon never respawns); verify with
grep supervise-daemon /etc/init.d/orva.
Image swap: ~5 seconds (orvad shuts down cleanly within TimeoutStopSec=30s,
new image starts in ~2 seconds, healthcheck passes within 15 s).
In-flight invocations during the swap: their HTTP connection drops. Retry logic at the caller (or the SDK) handles it.
- Body size: increase the proxy's body limit if you'll deploy
large tarballs. Default Orva cap is 6 MB for invoke bodies (an
over-limit body now returns
413 PAYLOAD_TOO_LARGE, including chunked uploads that carry noContent-Length); the deploy endpoint accepts up to 50 MB, a compile-time constant (builder.DefaultMaxCodeSize) with no config key. - Read timeout: must exceed the longest function
timeout_ms. SSE streams need a long read timeout (≥ 5 min recommended). X-Forwarded-For: Orva ignores this header (andX-Real-IP) unless you setORVA_TRUSTED_PROXY=true. Left off, every rate limiter — per-functionrate_limit_per_min, the login brute-force throttle, and the OAuth dynamic-registration limiter that is the only abuse control on the unauthenticatedPOST /register— keys on your proxy's own address, so every client behind it shares one bucket. Set it whenever a proxy fronts Orva, and not before: trusting a client-settable value means a caller can have no rate limit at all simply by varying it. When on, Orva reads the rightmost entry, which is the one your proxy appended ($proxy_add_x_forwarded_forin the nginx example above; Caddy and Traefik append by default). Anything further left was sent by the client. Also make sure Orva is reachable only through the proxy —ORVA_HOST=127.0.0.1, or a firewall — because the flag is an assertion that the peer always is the proxy. Make sure that proxy is the one writingX-Forwarded-For: a config that sets onlyX-Real-IPand passes the client'sX-Forwarded-Forthrough untouched leaves the bucket client-chosen even with the flag on.X-Forwarded-Proto: forward it (the nginx example above does) and Orva marks session cookiesSecureautomatically, withoutORVA_SECURE_COOKIES.- HTTP/2: helps with the dashboard's parallel API calls. SSE works over both HTTP/1.1 and HTTP/2.
- WebSockets: not used by Orva. SSE-only.
Not supported. Orva is single-host by design. Two patterns to scale:
- Vertical: bigger box, more RAM. The host-wide sandbox concurrency
ceiling scales automatically with CPU count (
NumCPU × 64, floor 200), so adding CPUs raises it. This is the easy answer for sub-1000-req/s aggregate workloads. - Stamp out copies: run multiple independent Orva instances, shard functions across them at the LB layer (deterministic hash on function name → host). Each instance has its own SQLite, its own data dir. No state sharing.
If you genuinely need clustered serverless, this isn't the platform.
Three integration points:
GET /api/v1/system/metrics— Prometheus text format. Scrape from your existing Prometheus.GET /api/v1/system/health— health probe for load balancers.200+{"status":"healthy", …}when up;503+{"status":"degraded"}when the database ping fails. Match onhealthy, notok.- Structured logs — orvad only ever emits JSON, so ship stdout straight to your aggregator (Loki, Elasticsearch, Datadog, whatever).
The dashboard's live metrics tiles + invocation log are an alternative to Prometheus for small deployments.
- HTTPS terminator in front (caddy / nginx / traefik / cloudflared)
-
network_mode: noneis the default — verify your functions stayed on it (operator can opt intoegressper-function for outbound HTTPS) - Egress policy reviewed on the Egress controls page. Note that it also filters
orvad's own outbound calls, so enabling a broad RFC1918 rule
(
10.0.0.0/8and friends) blocks internal package mirrors, LAN-hosted AI providers, and internal webhook targets too — block a narrower CIDR instead.status.enforcedmust betrue; if it isfalse, egress functions will not spawn at all (SECURITY.md) - Bootstrap admin key rotated — issue a new key via the dashboard
and delete the bootstrap one (or move
.admin-keyout-of-band) - API keys for clients have only the permissions they need
(
invokeonly is enough for end-users; reserveadminfor ops) - Backup automation in place (see Backups above)
- Disk growth monitored —
versions_to_keep× deps size × num functions can chew through GBs -
docs/SECURITY.mdread end-to-end so you understand what's protected and what isn't
See docs/OPERATIONS.md for the runbook.