Self-Hosting
Docker (cloud relay)
Run the Cyborg7 cloud relay with Docker Compose: relay, Redis and a one-shot migrate job, backed by your own PostgreSQL.
The repo ships a deploy/docker-compose.yml that runs the cloud relay, the standalone broker that browser and desktop guests connect to, with no local agents. It brings up the relay, Redis and a one-shot migrate job. You supply PostgreSQL (managed or your own) through DATABASE_URL.
Services
relay: the Hono HTTP + WebSocket gateway (built fromdeploy/Dockerfile.relay), listening on:9100inside the container;RELAY_PORTpicks the host port it is published on. The image also builds the web UI (packages/ui) and the relay serves it at/(the boot log saysServing web UI from /app/packages/ui/build), so you log in from a browser atRELAY_PUBLIC_URL. The UI is compiled with that URL baked in; see below.migrate: runs once before the relay starts: applies pending database migrations toDATABASE_URLand exits. Same image, same entrypoint the cloud deploy uses (packages/server/src/server/cyborg/db/migrate-cli.ts).redis:redis:7-alpine, used for shared rate-limit counters and multi-instance pub/sub. Optional for a single relay. Published on the host at6379, so a Redis already listening there blocksup.
PostgreSQL is not bundled. Point DATABASE_URL at your database.
Configure
Three values are required, and docker compose up refuses to start without them: DATABASE_URL, CYBORG7_JWT_SECRET and RELAY_PUBLIC_URL. Generate the secrets first. The script creates deploy/.env and writes three strong signing secrets into it (it refuses to touch a file that already has a CYBORG7_JWT_SECRET, so run it before adding anything else):
tools/gen-selfhost-secrets.sh deploy/.env
Then add the rest to deploy/.env:
DATABASE_URL=postgres://user:pass@your-db-host:5432/cyborg7
RELAY_PUBLIC_URL=https://relay.example.com # what browsers type; see below
CYBORG7_TOKEN_ENC_KEY=... # openssl rand -base64 32; see below
RESEND_API_KEY=... # signup OTP email (production)
EMAIL_FROM="Cyborg7 <onboarding@yourdomain.com>"
RELAY_PORT=9100 # host port; the container always listens on 9100
The relay container also loads every other variable in deploy/.env (env_file in deploy/docker-compose.yml, Compose 2.24 or newer), so optional settings such as ASSETS_BACKEND or S3_ASSETS_BUCKET reach it without editing the compose file. Values fixed in the compose environment: list win over .env. REDIS_URL is one of them: it points at the bundled redis service, so to use a different Redis, edit deploy/docker-compose.yml. Run docker compose config to see what the relay will get.
Public URL
RELAY_PUBLIC_URL is the origin browsers use to reach this relay, without a trailing slash (http://localhost:9100 for a local try-out). It is compiled into the web UI when the image is built (VITE_RELAY_URL), because the UI has no same-origin mode: built without it, the UI is hard-wired to the Cyborg7 cloud relay and would send your users’ logins there. That is why compose refuses to build without it, and why changing it means docker compose build again. The relay reads the same value at runtime for media links, Slack avatars and the MCP connection URL. One pre-existing exception: the UI’s client-error beacon (@cyborg7/observability) follows the saved session’s relay once someone has logged in on that browser, and before that first login it still reports script errors to relay.cyborg7.com.
TLS to Postgres
By default the relay negotiates TLS (without verifying the certificate) to any host that is not loopback, and from inside a container every host is non-loopback, host.docker.internal included. A Postgres that speaks no TLS therefore fails migrate with The server does not support SSL connections. To fix it, append ?sslmode=disable to DATABASE_URL, or set PG_SSL_MODE=disable in deploy/.env. For a managed database whose certificate you want verified, set PG_SSL_MODE=verify-full (it verifies against the bundled Amazon RDS CA; PG_SSL_ROOT_CERT overrides it with a PEM path inside the container).
Integration tokens
CYBORG7_TOKEN_ENC_KEY (base64, 32 bytes) encrypts stored Slack/Jira/GitHub/ClickUp tokens and webhook secrets. Without it the relay still boots and logs [task-sync-crypto] CYBORG7_TOKEN_ENC_KEY is unset, but every new integration credential write is refused, and rows already encrypted under a key you lost cannot be read. Set it before connecting an integration and keep it with your other secrets.
Run
cd deploy
docker compose up -d
# relay at http://localhost:9100 (health: GET /api/health)
docker compose logs -f relay
up builds the image, runs migrate against DATABASE_URL, and starts the relay only after migrate has exited successfully (depends_on: service_completed_successfully) and Redis is healthy (service_healthy). Redis data persists in the redis-data volume.
The first build installs the relay’s slice of the pnpm workspace (about 900 packages), compiles the workspace libraries the relay loads at runtime, builds the web UI in a parallel stage (about 80 seconds of that; its toolchain never enters the runtime image), and ends with an import smoke-check of the whole relay; expect around 2.5 minutes plus the npm download, and a 1.3 GB image. A build that fails on the smoke-check step means a dependency the relay imports is missing from the image. It does not indicate a bad configuration. Rebuilds after a source change reuse the install layers. On a fresh database migrate takes under a minute, and the relay answers GET /api/health with {"status":"ok", ... "pg":true,"redis":true} a few seconds after it starts.
Migrations
The relay does not apply migrations at boot; the migrate service does, on every docker compose up. Pending migrations are applied and already-applied ones are skipped, so re-running is safe. To run it by hand (after pulling a new version, or to see why a boot failed):
cd deploy
docker compose run --rm migrate
One migration (0072_message_embeddings) wants the pgvector extension. On a Postgres without it (the stock postgres:16 image does not ship it) that migration is a clean no-op: migrate still exits 0 and the relay boots, only the message-embedding features stay off. To enable them use an image that carries the extension (for example pgvector/pgvector:pg16) and have a superuser run CREATE EXTENSION vector; once; the next migrate creates the tables.
Going to production
- Put the relay behind TLS (
wss://) through a reverse proxy or load balancer. Daemon and guest sockets should never cross untrusted networks in the clear. - The relay process runs as the unprivileged
nodeuser (uid 1000) inside its container, not root. WithASSETS_BACKEND=fs, a customASSETS_FS_DIR(or a volume mounted there) must be writable by uid 1000. Treat the container like any other service: no host mounts it does not need, no--privileged. - Keep
DATABASE_URLpointed at a managed or secured PostgreSQL. Never commit credentials. - Set
RESEND_API_KEYso sign-up one-time codes can be delivered.
Machines that run agents do not need any of this. They only need to reach the relay. See the self-hosting overview and the production checklist.