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
- Docker with Compose v2.
- A Linux host or local Docker environment.
- Ports for app, room-state, LiveKit, and MinIO. The remote-browser executor remains private to the compose network; Caddy exposes only its token-protected
/framesWebSocket. - For public HTTPS deployment: domains or test hostnames for app/state/livekit/browser endpoints.
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
CONTROL_PLANE_ADMIN_TOKEN: replace with a strong secret.STATE_TOKEN_SECRET: replace with a strong secret.REMOTE_BROWSER_TOKEN_SECRET: replace with a strong secret.VRATA_INTERNAL_SERVICE_TOKEN: replace with a strong secret.REMOTE_BROWSER_INTERNAL_TOKEN: use a separate strong secret for the experimental executor boundary.POSTGRES_PASSWORD: replace with a strong password.MINIO_ROOT_USERandMINIO_ROOT_PASSWORD: replace with production values.VRATA_APP_BASE_URL,ROOM_STATE_PUBLIC_URL,LIVEKIT_URL, andREMOTE_BROWSER_PUBLIC_URL: set to the URLs that browsers can reach.LIVEKIT_NODE_IP: set to the public IP address advertised by the LiveKit node. Production preflight rejects loopback, private, reserved, and placeholder values.VRATA_LIVEKIT_TCP_PORTandVRATA_LIVEKIT_UDP_PORT: open these directly on the host for LiveKit WebRTC ICE/TCP fallback and UDP media traffic.
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:
postgres-datafor room/control-plane data;minio-datafor scene bundle objects;caddy-dataandcaddy-configfor Caddy state.
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:
- prepare a scene bundle with
scene.jsonand assets; - upload it to MinIO or S3-compatible storage;
- register scene bundle metadata through the API;
- bind the bundle to a room;
- open the room URL and verify diagnostics.
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:
80/tcpand443/tcpfor Caddy ACME and HTTPS application traffic.VRATA_LIVEKIT_TCP_PORT/tcp, default7881/tcp, for WebRTC ICE/TCP fallback.VRATA_LIVEKIT_UDP_PORT/udp, default7882/udp, for direct UDP media.LIVEKIT_TURN_TLS_PORT/tcp, default5349/tcp, only whenLIVEKIT_TURN_ENABLED=true.LIVEKIT_TURN_UDP_PORT/udp, default3478/udp, only whenLIVEKIT_TURN_ENABLED=true.LIVEKIT_TURN_RELAY_RANGE_START-LIVEKIT_TURN_RELAY_RANGE_END/udp, default50000-50100/udp, only whenLIVEKIT_TURN_ENABLED=trueand your firewall requires an explicit relay range.
DNS requirements:
VRATA_APP_DOMAINpoints to the public host.VRATA_STATE_DOMAINpoints to the same host unless room-state is split out.VRATA_LIVEKIT_DOMAINpoints to the same host or the LiveKit host and must matchLIVEKIT_URL.LIVEKIT_TURN_DOMAINshould be a separate hostname when TURN/TLS is enabled.
TURN/TLS options:
- Leave
LIVEKIT_TURN_ENABLED=falsefor a public signaling-only profile. - Set
LIVEKIT_TURN_ENABLED=trueto advertise LiveKit embedded TURN. - With
LIVEKIT_TURN_EXTERNAL_TLS=true, terminate TURN/TLS at an external layer-4 load balancer and forward toLIVEKIT_TURN_TLS_PORT. - With
LIVEKIT_TURN_EXTERNAL_TLS=false, provideLIVEKIT_TURN_CERT_FILEandLIVEKIT_TURN_KEY_FILEinside the LiveKit container through a compose override or host mount. - Do not reuse
VRATA_LIVEKIT_DOMAINasLIVEKIT_TURN_DOMAIN; production preflight rejects this to keep signaling and TURN/TLS routing explicit.
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
- Upgrade using explicit SemVer image tags, for example
0.1.1. - Do not use
latestas the upgrade target. - Back up Postgres and MinIO before every minor upgrade.
- Downgrading database schema is not guaranteed.
- Rollback means returning to the previous app image tag without deleting volumes.
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
- Postgres: a
pg_dump --clean --if-exists --no-owner --no-privilegesSQL dump. - MinIO/S3-compatible compose storage: mirrored bucket objects, an object inventory file, and bucket download policy metadata when available.
- Platform metadata: manifest schema version, created timestamp, Vrata package version,
IMAGE_TAG, git commit, compose profile, compose/env file names, artifact sizes, and SHA-256 checksums. - Compose image snapshot:
docker compose imagesoutput for operator diagnostics. - Staging exports can include the checksum-bound accepted release record and all Compose override filenames. See
docs/deployment-staging.mdfor the staging-specific wrapper.
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:
- manifest schema and required source metadata;
- artifact paths cannot escape the backup directory;
- required Postgres and MinIO inventory artifacts exist;
- artifact sizes and SHA-256 checksums match the manifest.
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
- Stop writes to the affected deployment if possible.
- Pick the newest verified backup whose manifest passes
pnpm backup:validate. - Restore with
--confirm-restoreand a reachable--smoke-base-url. - If the app image is the problem rather than data, use
pnpm rollback:composewith the previous explicit image tag. - Run the smoke command and record the backup directory, restored
IMAGE_TAG, git commit, and smoke result in the incident notes.
Limitations
- Point-in-time recovery and incremental object backups are out of scope for
0.1. - Downgrading database schema is not guaranteed. Backups are mandatory before minor upgrades.
- The scripts target the compose-managed Postgres and MinIO profiles. External managed databases or object stores need provider-specific snapshots in addition to this runbook.
Source: docs/self-hosting.md, docs/upgrades.md, docs/backup-restore.md in vrata-labs/platform at revision 380b2e7ff1dd, license Apache-2.0. GitHub