# Deploy and backups

> One Node 22 process with no runtime dependencies and one SQLite file. Run it in your own VPC, keep the master key safe, back up the evidence, and know what failover does and does not exist.

Source: https://immiscible.fly.dev/docs/guides/deploy-and-backups

Hosted plans run in the EU (Frankfurt). Enterprise customers can run Immiscible inside their own VPC, where nothing leaves the network and Immiscible adds no sub-processor at all. Your security review covers Node and the code; there is no package supply chain beyond that.

## Run it

```bash
docker build -t immiscible .
docker run -d --name immiscible -p 8787:8787 \
  -v imm-data:/data --read-only --tmpfs /tmp \
  -e IMMISCIBLE_MODE=selfhost \
  -e PUBLIC_URL=https://immiscible.internal.example \
  -e IMMISCIBLE_MASTER_KEY="$(openssl rand -base64 32)" \
  -e IMMISCIBLE_ADMIN_EMAIL=platform@example.com \
  -e IMMISCIBLE_ADMIN_PASSWORD="change-me-to-something-long" \
  -e ANTHROPIC_API_KEY=... -e OPENAI_API_KEY=... \
  immiscible
```

The image runs as an unprivileged user with a fixed uid (10001); the code is read-only to it and the only writable path is `/data`, so the root filesystem can be mounted read-only. `fly.toml`, `render.yaml` and `docker-compose.yml` in the repository are working starting points.

In self-hosted mode there is exactly one workspace. The owner comes from `IMMISCIBLE_ADMIN_EMAIL` and `IMMISCIBLE_ADMIN_PASSWORD`; sign-up closes once that owner exists, and everyone else is invited. Provider keys may come from the environment.

## Configuration

| Variable | Meaning |
|---|---|
| `PUBLIC_URL` | the address people and agents use; links, receipts' `iss` and OAuth metadata come from it |
| `IMMISCIBLE_MASTER_KEY` | 32 bytes, base64: seals stored provider keys, upstream credentials and secrets. Required in production |
| `IMMISCIBLE_MODE` | `selfhost` for one workspace; unset for the hosted, multi-workspace mode |
| `DATA_DIR`, `DATABASE_FILE` | where the SQLite file lives (default `$DATA_DIR/immiscible.db`) |
| `PORT` | default `8787` |
| `LOG_FORMAT` | `json` for JSON lines on stdout |
| `IMMISCIBLE_KEY_RPM`, `IMMISCIBLE_AGENT_RPM`, `IMMISCIBLE_HOOK_RPM`, `IMMISCIBLE_VERIFY_RPM` | rate limits per key, per agent, for the Claude Code hook's tool calls per agent, and for public receipt verification |
| `IMMISCIBLE_SESSION_IDLE_MINUTES`, `IMMISCIBLE_SESSION_MAX_DAYS` | console session lifetimes |

Sign-in providers, Slack and Teams have their own variables: see [set up sign-in](https://immiscible.fly.dev/docs/guides/sign-in.md) and [approvals in Slack and Teams](https://immiscible.fly.dev/docs/guides/approvals-in-chat.md).

> **Danger**
> Keep `IMMISCIBLE_MASTER_KEY` in your secrets manager, never in the image. Lose it and every sealed credential must be entered again; leak it and every one must be rotated.

## Health

| Route | Use |
|---|---|
| [`GET /healthz`](https://immiscible.fly.dev/docs/api/get-healthz.md) | liveness: the process answers, with its version and uptime |
| [`GET /readyz`](https://immiscible.fly.dev/docs/api/get-readyz.md) | readiness: the database answers and its schema is the one this build expects; `503` otherwise |

`npm run check` runs the pre-flight checks a deployment should pass before it takes traffic.

## Backups

```bash
node scripts/backup.mjs /backups
```

Writes a consistent copy of the database while the gateway keeps serving, checks the copy opens and counts its ledger, and keeps the newest 14 (`BACKUP_KEEP`). The evidence ledger is inside it. Run it at least daily, from cron or your platform's job runner, and **ship the copy off the machine**: a backup on the same volume is not a backup. Keep copies for as long as your record-keeping obligations require.

Rehearse a restore, and time it, before you rely on it. A restore is a copy of the file into `DATA_DIR` and a start; then verify the evidence:

```bash
node scripts/verify-evidence.mjs bundle.json --keys keys-you-kept.json
```

### Restore drill

With encrypted backups going to a bucket, a drill fetches the newest one, decrypts it into a scratch file and checks every evidence chain against its manifest, without touching live data:

```bash
immiscible-server restore --from-bucket latest --to /data/drill.db
```

The result, pass or fail and how long it took, is kept beside the backups as `restore-drill.json`. Security posture in the console and `trust.json` (`backup.lastRestoreDrill`) report it. Delete the scratch file afterwards.

## Several processes, one host

Several Immiscible processes on one machine can share one database file in WAL mode. Everything they must agree on is in that file and changed in short `BEGIN IMMEDIATE` transactions: the evidence chain (one chain, no forks), rate limits, ring-fence counters, provider breakers, freezes and their ceilings, and workspace settings, written by compare and swap so two processes changing different settings both keep their change. A few figures (budget spend, key counters) are flushed on a one-second debounce, so one process's view can lag another's by about a second. Evidence is never debounced.

SQLite allows one writer at a time, so this shape scales with cores on one host, not with hosts. **Never put the database file on a network file system** (NFS, SMB, EFS): SQLite's locking is not reliable there.

## Failover

Failover across hosts is not built in. Two ways to get it today, both outside Immiscible:

- **Litestream** replicates the SQLite file to object storage continuously. It is a backup with a recovery point of seconds, not a hot standby: to fail over, restore onto a new host and start there. Run exactly one writing host at a time.
- **LiteFS** replicates to read replicas with one primary. Immiscible does not route writes to the primary or handle promotion, so run it only on the primary and treat promotion as an operator action.

Either way a host failure means a short outage while the standby takes over, and with Litestream the last few seconds of writes can be lost. If your recovery objectives cannot accept that, run it on Postgres (`DATABASE_URL`), where your provider's replication and point-in-time recovery apply. One server process per database is what is tested.

## Upgrading

Take a backup, stop, replace the image, start. Schema migrations run on boot and are forward-only, and `/readyz` stays `503` until the schema matches the build.
