Self-hosting

This document describes the target self-host path for Vrata 0.1 beta.

Until v0.1.0 is released, public GHCR images may not exist. From a source checkout, use --build to build images locally.

Requirements

Source-Build Quickstart

From the repository root:

docker compose --env-file infra/docker/.env.selfhost.example -f infra/docker/compose.selfhost.yml config
docker compose --env-file infra/docker/.env.selfhost.example -f infra/docker/compose.selfhost.yml up -d --build

Smoke checks:

curl -fsS http://127.0.0.1:4000/health
curl -fsS http://127.0.0.1:4000/rooms/demo-room
curl -fsS http://127.0.0.1:4000/control-plane

Teardown for local evaluation:

docker compose --env-file infra/docker/.env.selfhost.example -f infra/docker/compose.selfhost.yml down

To remove persistent data from a test deployment:

docker compose --env-file infra/docker/.env.selfhost.example -f infra/docker/compose.selfhost.yml down -v

Release Image Path

After v0.1.0, copy the example env file and select a release tag:

cp infra/docker/.env.selfhost.example infra/docker/.env.selfhost

Edit infra/docker/.env.selfhost and set real secrets, domains, and IMAGE_TAG.

Then run:

docker compose --env-file infra/docker/.env.selfhost -f infra/docker/compose.selfhost.yml pull
docker compose --env-file infra/docker/.env.selfhost -f infra/docker/compose.selfhost.yml up -d

Required Configuration

The bundled compose file runs LiveKit in --dev mode for the 0.1 beta quickstart, so the local defaults are LIVEKIT_API_KEY=devkey and LIVEKIT_API_SECRET=secret. Replace this with a hardened LiveKit configuration before exposing a deployment beyond evaluation use.

Remote browser is experimental and defaults to REMOTE_BROWSER_ENABLED=false. To opt in, set a narrow REMOTE_BROWSER_ALLOWED_ORIGINS list and review REMOTE_BROWSER_MAX_SESSIONS, REMOTE_BROWSER_SESSION_TTL_SECONDS, REMOTE_BROWSER_CPU_LIMIT, REMOTE_BROWSER_MEMORY_LIMIT, and REMOTE_BROWSER_PIDS_LIMIT. Do not expose the executor port directly to untrusted networks.

Persistent Data

The self-host compose file stores data in Docker volumes:

Back up these volumes before upgrades. Use pnpm backup:compose, validate the produced manifest with pnpm backup:validate, and keep the artifacts outside git. See docs/backup-restore.md and docs/upgrades.md.

Adding A Space

The minimal 0.1 flow is API-driven:

See docs/scene-bundle-contract.md for the bundle layout.

Production Notes

Do not expose a self-hosted deployment with example secrets. The compose.selfhost.yml path is for local evaluation and keeps LiveKit in --dev mode.

Use the production-safe profile when preparing a public host:

cp infra/docker/.env.production.example infra/docker/.env.production

Edit infra/docker/.env.production and replace every REPLACE_WITH_* value, example hostname, and public URL. Public browser-facing URLs must use https:// or wss:// unless you are doing an explicit loopback-only preflight test with VRATA_ALLOW_INSECURE_PRODUCTION_URLS=true.

Run the preflight before starting services:

node tools/validate-production-config.mjs --env-file infra/docker/.env.production

Validate the compose model:

docker compose --env-file infra/docker/.env.production -f infra/docker/compose.production.yml config

Start from source-built images:

docker compose --env-file infra/docker/.env.production -f infra/docker/compose.production.yml up -d --build

Or start from published release images after v0.1.0:

docker compose --env-file infra/docker/.env.production -f infra/docker/compose.production.yml pull
docker compose --env-file infra/docker/.env.production -f infra/docker/compose.production.yml up -d

Smoke checks:

curl -fsS "$VRATA_APP_BASE_URL/health"
curl -fsS "$VRATA_APP_BASE_URL/rooms/demo-room"
curl -fsS "$VRATA_APP_BASE_URL/diagnostics"
curl -fsS "$CONTROL_PLANE_PUBLIC_URL"

Production preflight blocks startup when it detects missing required variables, public http:// / ws:// URLs, wildcard CORS, dev-role query mode, placeholder secrets, weak secrets, duplicate secret values, invalid LiveKit public IP/ports, or a LIVEKIT_URL host that does not match VRATA_LIVEKIT_DOMAIN. Diagnostics include variable names and reason codes only; secret values must not be printed.

LiveKit / TURN Network Profile

The production compose profile runs LiveKit from an explicit LIVEKIT_CONFIG body instead of --dev. Caddy terminates HTTPS for the signaling endpoint at LIVEKIT_URL, for example wss://livekit.example.com, and forwards it to livekit:7880.

Open these inbound ports on the host or cloud firewall:

DNS requirements:

TURN/TLS options:

The API /health and /health/ready payloads expose non-secret LiveKit deployment diagnostics under dependencies.livekitConfig: signaling TLS status, configured host, TURN enabled flag, TURN domain, TURN ports, and relay range. Runtime diagnostics posted to POST /api/rooms/:roomId/diagnostics include media.webrtc with transport states, selected ICE candidate pair state, candidate types, and TURN relay availability/use booleans. The runtime snapshot intentionally omits raw ICE candidate addresses and ports.

After public DNS/HTTPS is configured, open $VRATA_APP_BASE_URL/diagnostics?roomId=demo-room&autorun=1 in a browser. Use skipMic=1 when you need to avoid a microphone permission prompt but still want API, room-state WSS, storage, LiveKit, and WebRTC/TURN checks. Copy the JSON report into support issues; token-like query values and secret-like keys are redacted.

POST /api/tokens/media and POST /api/tokens/remote-browser-media return livekit_config_invalid in production if LiveKit URL/key/secret are missing, insecure, or still using dev credentials.

Rollback for this profile is metadata/config-only: restore the previous .env.production and image tag through pnpm rollback:compose, then smoke the app URL. If the production profile itself is the problem, stop it and return to the previously used documented compose file. This task does not introduce point-in-time database recovery.

Upgrades And Rollback

This document describes the target upgrade policy for Vrata 0.1 self-host deployments.

Policy

Backup Before Upgrade

Create and validate a compose backup before changing image tags:

pnpm backup:compose -- --env-file infra/docker/.env.selfhost --compose-file infra/docker/compose.selfhost.yml --output-dir backups
pnpm backup:validate -- --backup-dir backups/vrata-<timestamp>-<image-tag>

The backup command exports Postgres, MinIO bucket objects, object inventory, bucket policy metadata when available, image metadata, and a checksum manifest. See docs/backup-restore.md for restore and disaster recovery details.

Upgrade

Edit infra/docker/.env.selfhost:

IMAGE_TAG=0.1.1

Then run:

docker compose --env-file infra/docker/.env.selfhost -f infra/docker/compose.selfhost.yml pull
docker compose --env-file infra/docker/.env.selfhost -f infra/docker/compose.selfhost.yml up -d

Smoke checks:

curl -fsS "$VRATA_APP_BASE_URL/health"
curl -fsS "$VRATA_APP_BASE_URL/rooms/demo-room"
curl -fsS "$VRATA_APP_BASE_URL/control-plane"

Rollback

If smoke fails, set IMAGE_TAG back to the previous known-good tag and restart:

pnpm rollback:compose -- \
  --previous-image-tag 0.1.0 \
  --env-file infra/docker/.env.selfhost \
  --compose-file infra/docker/compose.selfhost.yml \
  --smoke-base-url "$VRATA_APP_BASE_URL" \
  --confirm-rollback

Do not run down -v during rollback unless you intentionally want to delete data.

Migration Status

Current schema setup is bootstrap-oriented and uses idempotent table/column creation in the API storage layer. A dedicated migration runner is a planned requirement before Vrata can claim stronger production upgrade guarantees.

Backup, Restore, And Rollback

Vrata self-host and production compose profiles use named Docker volumes for persistent state. tools/backup-restore.mjs provides the operator path for exporting that state, validating backup artifacts, restoring it, rolling back image tags without deleting volumes, and running smoke checks.

What Is Backed Up

Backups are written under backups/ by default. That directory is ignored by git because backup artifacts can contain private room metadata and scene assets.

Create A Backup

From the repository root:

pnpm backup:compose -- --env-file infra/docker/.env.selfhost --compose-file infra/docker/compose.selfhost.yml --output-dir backups

For the production-safe profile, use infra/docker/.env.production and infra/docker/compose.production.yml.

The command fails non-zero if Postgres dump, MinIO mirror, image snapshot, or manifest writing fails. Logs print paths and stable failure codes only; secret values from the env file are redacted from command stderr.

Validate A Backup

pnpm backup:validate -- --backup-dir backups/vrata-<timestamp>-<image-tag>

Validation checks:

Corrupt or incomplete manifests fail non-zero before restore starts.

Restore

Restore is destructive for the target deployment state: it applies the SQL dump to Postgres and mirrors the backed-up object set back into MinIO with --remove.

Do not run restore against a production host unless you have explicitly chosen that backup as the recovery point.

pnpm restore:compose -- \
  --backup-dir backups/vrata-<timestamp>-<image-tag> \
  --env-file infra/docker/.env.selfhost \
  --compose-file infra/docker/compose.selfhost.yml \
  --smoke-base-url http://127.0.0.1:4000 \
  --smoke-room-id demo-room \
  --confirm-restore

Restore always validates the manifest first. After applying the dump and objects, it runs smoke checks against /health, /rooms/:roomId, /api/rooms/:roomId/manifest, and the manifest scene bundle URL when one is present.

Rollback Image Tag

Rollback is config/image-only. It validates the target tag and smoke URL before editing the env file, updates IMAGE_TAG, pulls app service images, and runs docker compose up -d --no-build. It does not run docker compose down -v, and it does not delete Postgres or MinIO volumes.

pnpm rollback:compose -- \
  --previous-image-tag 0.1.0 \
  --env-file infra/docker/.env.selfhost \
  --compose-file infra/docker/compose.selfhost.yml \
  --smoke-base-url http://127.0.0.1:4000 \
  --smoke-room-id demo-room \
  --confirm-rollback

The previous env file is copied to backups/rollback-env/ before IMAGE_TAG is changed. latest is rejected as a rollback target; use an explicit SemVer or immutable SHA tag.

Smoke Only

pnpm smoke:compose -- --smoke-base-url http://127.0.0.1:4000 --smoke-room-id demo-room

Use this after manual recovery steps or storage maintenance.

Retention

Dry-run old backup deletion:

pnpm backup:prune -- --output-dir backups --retention-days 14

Apply deletion:

pnpm backup:prune -- --output-dir backups --retention-days 14 --confirm-prune

VRATA_BACKUP_RETENTION_DAYS can provide the default retention window. The prune command only considers directories whose names start with vrata-. It validates checksums before deleting a copy and preserves at least two newer verified backups. Incomplete or corrupt copies are kept for diagnosis and do not count as recovery points.

Disaster Path

  1. Stop writes to the affected deployment if possible.
  2. Pick the newest verified backup whose manifest passes pnpm backup:validate.
  3. Restore with --confirm-restore and a reachable --smoke-base-url.
  4. If the app image is the problem rather than data, use pnpm rollback:compose with the previous explicit image tag.
  5. Run the smoke command and record the backup directory, restored IMAGE_TAG, git commit, and smoke result in the incident notes.

Limitations

Source: docs/self-hosting.md, docs/upgrades.md, docs/backup-restore.md in vrata-labs/platform at revision 380b2e7ff1dd, license Apache-2.0. GitHub