Operating your server
This page covers day-to-day work on a running Casama server: upgrades, backups, monitoring, logs, audit-log forwarding and scaling, plus the Kubernetes Helm chart. The commands use the Casama CLI, which wraps install.sh and docker compose for your checkout.
Everyday commands
Run these from the checkout, or pass --dir ~/casama (or set CASAMA_SELFHOST_DIR).
| Task | Command |
|---|---|
| Status | casama selfhost status |
| Logs | casama selfhost logs -f (api and worker), or name services: logs migrate supervisor |
| Start | casama selfhost up |
| Stop | casama selfhost down (data is kept) |
| Upgrade | casama selfhost upgrade |
| Back up | casama selfhost backup --out /backups |
| Restore | casama selfhost restore /backups/<timestamp> |
Each one maps to plain Compose, if you prefer:
docker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env pscasama selfhost down stops the stack but leaves running bot computers alone, because the supervisor creates them outside Compose. After the next up, the worker archives and stops any that sat idle for 10 minutes.
Don't delete the volumes
docker compose down -v deletes the database, computer archives and certificates. The CLI never passes -v.
Upgrade
Back up first:
shcasama selfhost backup --out /backupsUpgrade:
shcasama selfhost upgradeThis runs
git pull --ff-onlyin the checkout, then./install.sh. The installer keepsinfra/compose/.env, adds any settings the new release introduced, pulls and rebuilds the images, applies new database migrations and restarts the services that changed.Check the result with
casama selfhost statusand the admin console.
casama selfhost upgrade runs the installer with no options. If you installed with --with-desktop, --monitoring or --web, upgrade by hand instead so those parts are rebuilt too:
git pull --ff-only
./install.sh --with-desktop --monitoring --webThe installer doesn't rebuild an existing web image. After an upgrade with --web, rebuild it:
docker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env --profile web build web
docker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env --profile web up -dNotes:
- Migrations run automatically in the one-shot
migrateservice at every start, once each, in order. They're tracked inpublic.casama_migrations. - Running computers keep the image they started with. They pick up the new image the next time they sleep and wake.
- If
git pull --ff-onlyfails, you have local changes to tracked files (for example an editeddocker-compose.yml). Commit them on a local branch or stash them, then try again.
Back up your server
casama selfhost backup --out /backupsEach run writes a timestamped folder, for example /backups/2026-11-11T09-30-00, with:
| File | Contents |
|---|---|
postgres.dump | The whole database (pg_dump custom format) |
objects.tar.gz | Computer archives and checkpoints from the objects volume |
env | A copy of infra/compose/.env, with every key |
--out defaults to casama-backups in the current directory. With S3 object storage, the archive only covers the local volume, so back up the bucket with your provider's tools.
Encrypt backups and keep a set number
Backups hold every key for your server, so encrypt them before they leave the machine. Casama uses age.
Install age (
apt install ageorbrew install age).On another machine, create a key pair:
shage-keygen -o backup.keyIt prints the public key (
age1...). Keepbackup.keyoff the server.Back up with encryption and retention:
shcasama selfhost backup --out /backups --encrypt --recipient age1... --keep 14 --keep-days 30
| Option | What it does |
|---|---|
--out <folder> | Where backup folders go (default casama-backups). |
--encrypt | Encrypts each file with age (postgres.dump.age and so on) and removes the plaintext. |
--recipient <key> | An age public key (age1...) or SSH public key to encrypt to. Repeat for more. |
--keep <n> | Keep only the newest n backups in --out. |
--keep-days <n> | Remove backups older than n days. With --keep too, a backup goes only when it's outside both. |
CASAMA_BACKUP_RECIPIENTS (comma-separated) can stand in for --recipient. Retention only removes the timestamped folders the CLI itself wrote.
Schedule backups
Run the backup daily with cron and copy the folder off the machine. For example, in the crontab of a user who can run Docker:
30 3 * * * casama selfhost backup --dir /opt/casama --out /backups --encrypt --recipient age1... --keep 14 >> /var/log/casama-backup.log 2>&1Then sync /backups to other storage with your usual tool.
Restore
restore replaces the current data with the backup.
casama selfhost restore /backups/2026-11-11T09-30-00
# encrypted backup:
casama selfhost restore /backups/2026-11-11T09-30-00 --identity backup.keyIt stops the API and worker, restores the database with pg_restore --clean, unpacks the computer archives into the objects volume, and starts the stack again. pg_restore prints warnings about objects Supabase owns; they're expected, and the data is restored.
restore doesn't touch infra/compose/.env. To restore onto a new machine:
- Clone the same release of the repository.
- Copy the backup's
envfile toinfra/compose/.env(decrypt it first withage -d -i backup.key -o infra/compose/.env env.ageif it's encrypted). - Run
./install.sh. It keeps that.env, so the new stack uses the same keys. - Run
casama selfhost restore <backup-folder>.
Test your restores
The backup holds the database, computer archives and .env. It doesn't include the other Docker volumes (for example db-config, which holds the database's own configuration, or Caddy's certificates). Restore onto a spare machine now and then, and check that you can sign in, that bots answer with their saved model keys, and that a computer wakes with its files.
Monitoring
The monitoring profile adds Prometheus, node-exporter and a Grafana dashboard.
Turn it on:
sh./install.sh --monitoringOr add the profile to a running stack:
shdocker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env --profile monitoring up -dGrafana listens on
127.0.0.1:3001on the server. From your computer, open an SSH tunnel:shssh -L 3001:127.0.0.1:3001 you@casama.example.comOpen
http://localhost:3001and sign in asadminwithGRAFANA_ADMIN_PASSWORDfrominfra/compose/.env. The Casama overview dashboard is already provisioned.
Prometheus keeps 30 days of data and isn't published on the host. It scrapes the API's /metrics inside the stack with METRICS_TOKEN, and node-exporter for host metrics. The config is in infra/monitoring/prometheus.yml.
Metrics
The API serves Prometheus metrics at /metrics with a bearer token (METRICS_TOKEN). Caddy doesn't route /metrics, so it's only reachable inside the stack. The values are computed from Postgres on each scrape.
| Metric | What it measures |
|---|---|
casama_runs_15m | Runs created in the last 15 minutes, by status |
casama_run_failure_ratio_15m | Failed runs out of finished runs, last 15 minutes |
casama_time_to_first_token_seconds | Run start to first model token, p50 and p95, last hour |
casama_computer_start_seconds | Computer start or wake time, p50 and p95, last hour |
casama_computers | Computers by state |
casama_cost_per_run_usd | Average cost of completed runs, last hour |
casama_queue_depth | Queued and running workflows, by queue |
casama_http_requests_total | API responses by status |
casama_http_request_seconds_sum, _count | Total API response time and responses timed |
Alerts
infra/monitoring/alerts.yml defines these rules:
| Alert | Fires when | Severity |
|---|---|---|
CasamaRunFailuresHigh | More than 5% of runs failed in 15 minutes (at least 20 finished), for 5 minutes | page |
CasamaSandboxMemoryHigh | A host is above 85% memory for 10 minutes | page |
CasamaTimeToFirstTokenSlow | p95 time to first token is above 3 seconds for 15 minutes | warn |
CasamaQueueBacklog | More than 200 runs are waiting, for 10 minutes | warn |
CasamaApiDown | Prometheus can't scrape the API for 2 minutes | page |
The stack doesn't include Alertmanager. Route alerts to email or chat with your own Alertmanager or with Grafana alerting.
The admin console
Instance owners see a health summary under Settings → admin console: the database, the worker, the sandbox supervisor, the live view relay, personal computers, email, error reporting, the enterprise license and telemetry, each with whether it's configured and healthy.
Health endpoints
| Endpoint | What it checks |
|---|---|
/healthz | The API process answers. Always 200 while it's up. |
/readyz | The API can reach the database. 503 with "degraded" otherwise. |
The supervisor has its own /healthz on port 8788, inside the stack.
Logs
casama selfhost logs -f # api and worker
casama selfhost logs migrate # database migrations
casama selfhost logs -f supervisor egress # computers and their internet access
casama selfhost logs auth # Supabase Auth: sign-in and emaillogs shows the last 200 lines of each service; -f follows. The API, worker, supervisor and egress proxy write JSON lines, one event each. The egress proxy logs each outbound connection a computer makes.
To see bot computers, which run outside Compose:
docker ps --filter name=casama-c-Set the API and worker's detail with LOG_LEVEL (see Advanced settings). For run-level traces, use Langfuse or an OpenTelemetry collector (see Telemetry and observability).
Audit-log forwarding
Audit-log forwarding streams each workspace's audit log to your own systems. It's an enterprise feature: it needs a CASAMA_LICENSE_KEY that includes audit_forwarding. Without one, the endpoints answer 402.
Workspace owners and admins manage forwarders through the API at /v1/orgs/{id}/audit-forwarders (GET to list, POST to add, DELETE /{forwarder_id} to remove). Each forwarder sends to one destination:
kind | config | secret |
|---|---|---|
webhook | url | Required: the signing secret |
otlp | url (Casama posts to <url>/v1/logs) | Optional: JSON of extra headers, for example an auth token |
s3 | bucket; optional endpoint, region (default us-east-1), prefix (default casama-audit) | Required: {"access_key_id":"...","secret_access_key":"..."} |
For example, a signed webhook:
curl -X POST https://casama.example.com/v1/orgs/$ORG_ID/audit-forwarders \
-H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
-d '{"kind":"webhook","config":{"url":"https://siem.example.com/casama"},"secret":"a-long-random-secret"}'$TOKEN is a signed-in admin's access token (see API). Add "from":"start" to send the whole history instead of starting from now.
How delivery works:
- The worker runs a pass every minute and sends new rows in order, in batches of up to 500.
- Webhook: JSON
{forwarder_id, events}with anX-Casama-Signature: t=<unix>,v1=<hex>header, the HMAC-SHA256 of<t>.<body>with your secret. - OTLP: each row becomes a log record under
service.name=casama, scopecasama.audit. - S3: one JSON Lines object per batch at
<prefix>/<workspace>/YYYY/MM/DD/<ms>-<first id>.jsonl, path-style, so AWS S3, R2 and MinIO work. - Delivery is at least once. A failed batch stays put (the forwarder's
last_errorsays why) and goes again on the next pass.
Secrets are stored in Supabase Vault.
Scaling
Worker concurrency. RUN_CONCURRENCY (default 20) is how many runs one worker processes at once. Raise it on a bigger machine, then run casama selfhost up. The worker opens up to RUN_CONCURRENCY + 5 database connections. Watch casama_queue_depth and the CasamaQueueBacklog alert to see when you need more.
Computer size. COMPUTER_MEMORY_MB (default 2048) sets each new computer's memory. Each computer also gets 1 CPU, 512 processes, a 1 GB /tmp and 512 MB of shared memory. Plan host memory as about 2 GB per running computer, plus the stack itself.
Warm pool. WARM_POOL_SIZE keeps that many headless computers started and unassigned, so a new computer starts in seconds instead of booting. A new computer with the default size claims one, and the pool refills after each claim and every minute. Desktop and custom-size computers always start cold. Each pool member uses memory while it waits. It works with the Docker provider only.
Idle computers. The worker archives computers to object storage and stops them after 10 minutes without activity, so only computers in use take memory.
Run computers on a separate host
On a public server, keep untrusted code and the Docker socket off the machine with your data. The supervisor, egress proxy and TURN relay must run together on the computer host: the supervisor attaches the proxy and the relay to each computer's network by container name.
On the computer host, clone the same release and copy
infra/compose/.envfrom the app host.In
infra/compose/docker-compose.ymlon the computer host, publish the supervisor's port on the private interface only, for example underservices.supervisor:yamlports: - "10.0.0.2:8788:8788"Build and start only those services:
shdocker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env build supervisor egress turn COMPOSE_PROFILES=images docker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env build computer-headless docker compose -f infra/compose/docker-compose.yml --env-file infra/compose/.env up -d supervisor egress turnOn both hosts, set
TURN_PUBLIC_HOSTto a name that points at the computer host, and open 3478 (UDP and TCP) there.On the app host, the compose file sets
SUPERVISOR_URL: http://casama-supervisor:8788in the sharedx-casamablock. Change it to the computer host's private address (http://10.0.0.2:8788), then stop and remove the localsupervisor,egressandturnservices.Connect the two hosts over a private network or WireGuard. The supervisor checks
SUPERVISOR_TOKENon every call, but it should never be reachable from the internet.
With computers on another host, their archives live in that host's objects volume. Back it up there, or switch to S3 object storage so both hosts see the same store. Add the computer host's node-exporter to infra/monitoring/prometheus.yml to get its memory alerts.
Kubernetes with Helm
infra/helm/casama runs the API, worker, supervisor and, optionally, the web app on Kubernetes. Computers run as pods in their own namespace through the supervisor's Kubernetes provider. The chart doesn't run Postgres or Supabase: point it at Supabase Cloud or your own Supabase (for example the Supabase Helm chart).
Install:
shhelm install casama infra/helm/casama -n casama --create-namespace \ --set publicUrl=https://casama.example.com \ --set pagesUrl=https://pages.casama.example.com \ --set supabase.url=https://xyz.supabase.co \ --set supabase.anonKey=... --set supabase.serviceRoleKey=... --set supabase.jwtSecret=... \ --set database.url=postgres://postgres:...@db.xyz.supabase.co:5432/postgres \ --set ingress.apiHost=casama.example.com --set ingress.pagesHost=pages.casama.example.comPoint Supabase Auth's "before user created" hook at
https://casama.example.com/hooks/auth/before-user-createdif you want the sign-up allowlist.Upgrade later with
helm upgradeand the same values. Migrations run first.
What the chart does:
- Migrations run as a pre-install and pre-upgrade Job. The migration files are copied out of the server image, so the chart and image never drift apart. The Job uses
database.adminUrl(defaults todatabase.url). - Secrets:
INTERNAL_TOKEN,SUPERVISOR_TOKEN,COMPUTER_TOKEN_SECRETandMETRICS_TOKENare generated once and kept across upgrades. A value you set wins, then the existing Secret, then a new random one. The Secret is kept on uninstall. Bring your own withexistingSecret. - Computers run as pods in
computers.namespace. The supervisor's Role there allows only pods and pod proxying. Setcomputers.runtimeClassto a gVisor or Kata runtime class if your cluster has one. - Ingress routes the API host's API paths to the API (and
/to the web app when it's on), and the Pages host to the API's Pages port. - The supervisor keeps computer archives on a 20 Gi
ReadWriteOncevolume, with one replica.
Values
| Value | Default | What it does |
|---|---|---|
image.registry | ghcr.io/casama-dev | Where the server, supervisor, computer-headless, computer-desktop and web images come from. |
image.tag | latest | Image tag for all of them. |
image.pullSecrets | [] | Pull secrets for a private registry. |
publicUrl | https://casama.example.com | The API's public address. |
pagesUrl | https://pages.casama.example.com | Pages' public address. |
supabase.url | https://your-project.supabase.co | Supabase address. |
supabase.anonKey | Required | Supabase anon key. |
supabase.serviceRoleKey | Required | Supabase service role key. |
supabase.jwtSecret | Required | Supabase JWT secret. |
database.url | Required | Full-access Postgres URL for the API. |
database.workerUrl | database.url | Least-privilege URL for the worker. |
database.adminUrl | database.url | URL migrations run with (supabase_admin on self-hosted Supabase). |
existingSecret | (empty) | Use your own Secret with the generated keys and CONTENT_ENCRYPTION_KEY. |
secrets.* | (empty: generated) | internalToken, supervisorToken, computerTokenSecret, metricsToken, contentEncryptionKey. |
extraEnv | {} | More environment for the API and worker, for example RESEND_API_KEY. |
api.replicas | 2 | API replicas. |
worker.replicas | 1 | Worker replicas. |
worker.runConcurrency | 20 | RUN_CONCURRENCY for the worker. |
supervisor.enabled | true | Run the supervisor and computers. |
computers.namespace | casama-computers | Namespace for computer pods. |
computers.create | true | Create that namespace. |
computers.runtimeClass | (empty) | Runtime class for computer pods (gVisor, Kata). |
computers.memoryMb | 2048 | Passed to the supervisor as COMPUTER_MEMORY_MB, which only the Docker provider reads. Computer pods get no limits from it. |
web.enabled | false | Run the web app. |
migrations.enabled | true | Run the migration Job. |
ingress.enabled | true | Create an Ingress. |
ingress.className | (empty) | Ingress class. |
ingress.apiHost | casama.example.com | Host for the API (and the web app). |
ingress.pagesHost | pages.casama.example.com | Host for Pages. |
ingress.tls | [] | Ingress TLS blocks. |
Each component also has a resources value.
What the chart leaves out
- Live view. The chart doesn't run a TURN relay, so desktop computers have no live view.
- The egress proxy. Computer pods use the cluster's network. Add a NetworkPolicy to the computers namespace that blocks private ranges and cloud metadata addresses.
- The warm pool, which needs the Docker provider.
- Computer resource limits. The Kubernetes provider sets pod limits only for a computer with its own size, so add a LimitRange to the computers namespace for default limits.
- Supervisor settings beyond the defaults.
extraEnvreaches the API and worker only. Hosted providers or S3 object storage on the supervisor need a change to its Deployment.
Check the chart without installing anything locally: scripts/check-helm.sh runs helm lint and helm template in the official Helm Docker image.
