Self-hosting
Deploying RetinueOS on a server: environment variables, Web Push, HTTPS/reverse proxies, and upgrades.
Deploying RetinueOS somewhere other than your own laptop: every env var, Web Push, and running it behind HTTPS. For local development instead, the README Quick start is enough — come back here once you're putting it on a server.
Prerequisites
- Docker and Docker Compose (
docker compose, the plugin form). - At least one LLM provider key:
ANTHROPIC_API_KEYand/orOPENAI_API_KEY. Without either, RetinueOS still lets you sign in but blocks everything else — there's no persona work it could do. - A domain or IP you control, if this needs to be reachable from outside the host. Web Push and any OAuth-based connector (Gmail, Calendar) require a real HTTPS origin — see HTTPS and reverse proxies.
Environment variables
.env.example is the source of truth — each var is documented inline
there, and docker-compose.yml wires all of them into the backend and
frontend services. Copy it and fill in what applies:
cp .env.example .env| Variable | Required | What it's for |
|---|---|---|
AUTH_PASSWORD | Yes | The one shared password gating the whole app (not multi-tenant). |
ANTHROPIC_API_KEY / OPENAI_API_KEY | At least one | Your own LLM provider keys. Personas run on whichever model you assign. |
BRAVE_SEARCH_API_KEY | No | Enables the built-in web_search tool. |
CREDENTIALS_ENCRYPTION_KEY | Only for authenticated MCP connectors | AES key for storing an MCP server's bearer token or OAuth client secret. Generate with openssl rand -base64 32. An unauthenticated MCP server doesn't need it; adding one that does, without this key set, fails at save time. |
BACKEND_URL | For OAuth connectors | The backend's externally-reachable origin; builds the /oauth/callback redirect URI. Must exactly match what you register with the OAuth provider. |
FRONTEND_ORIGIN | Yes, if not on defaults | Origin the backend allows via CORS — the browser-visible frontend URL. |
NEXT_PUBLIC_BACKEND_URL | Yes, if not on defaults | Public URL the browser uses to reach the backend. Inlined into the frontend bundle at build time — changing it needs docker compose build frontend, not just a restart. |
NOTIFY_WEBHOOK_URL | No | Webhook destination for job/routine outcomes (e.g. an ntfy.sh topic). |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT | No, all three together | Standards-based Web Push to browser devices. See below. |
Defaults (http://localhost:3000 / http://localhost:8080) are correct
for running Compose on your own machine and nowhere else.
Web Push (VAPID)
Browser push notifications need one VAPID key pair, generated once:
cd backend
npx web-push generate-vapid-keysPut the public/private key in .env as VAPID_PUBLIC_KEY /
VAPID_PRIVATE_KEY, and set VAPID_SUBJECT to an operator contact URI
(mailto:you@example.com). All three are required together — partial
config leaves push disabled. Rebuild and restart after changing them:
docker compose up --build -dThen, per device, open Settings → Notifications and enable
notifications on that device — RetinueOS only asks the browser for
permission after that click. NOTIFY_WEBHOOK_URL is independent of this
and keeps working with or without VAPID configured; both destinations are
attempted separately and one failing doesn't block the other.
Web Push requires a secure browser context. On a real deployment that
means HTTPS (see below); localhost is the only exception. On iPhone/iPad,
Home Screen install is required before notifications can be enabled at
all — see the
WebKit platform notes.
Connecting Google (Gmail, Calendar) and other MCP servers
External tools — Gmail, Calendar, anything else — are added per persona
through Connections (/settings/mcp), not env vars. Only remote HTTPS
MCP servers are supported; there's no local stdio option. Full walkthrough,
including the Google-hosted servers (Developer Preview) and a self-hosted
fallback:
docs/CONNECTORS.md— the general mechanism and the Google-hosted Gmail/Calendar servers.docs/SELF_HOSTED_GOOGLE_WORKSPACE_MCP.md— run your own Google Workspace MCP server if your project isn't enrolled in the Developer Preview.
CREDENTIALS_ENCRYPTION_KEY (above) must be set before you can save a
connector's credentials.
HTTPS and reverse proxies
RetinueOS itself doesn't terminate TLS — backend and frontend are
plain HTTP inside Compose, on ports 8080 and 3000. Put a reverse proxy
(Caddy, nginx, Traefik) in front for anything beyond localhost. Two
things depend on this directly:
- The auth password travels as a header (
X-Auth-Password) on every request, not a cookie. Over plain HTTP, that's a plaintext password on the wire. - Web Push requires a secure context — browsers refuse to register a push subscription over insecure HTTP.
Stop publishing the ports the proxy front-ends. docker-compose.yml's
default ports: mappings (8080:8080, 3000:3000, and — separately —
5432:5432) bind on every host interface, not just loopback. A reverse
proxy in front of frontend/backend doesn't stop a client from reaching
them directly on those ports, bypassing HTTPS entirely, and it doesn't
stop anyone from reaching Postgres on 5432 with the checked-in default
retinue/retinue credentials. Compose merges an override file's list
fields (ports included) by concatenating them onto the base file's, not
replacing them — an override can't clear a ports: entry, only add to it.
Edit the ports: mappings in docker-compose.yml itself: drop the
postgres and, once a proxy is in front of them, the backend/frontend
entries too — Compose services already reach each other by name over the
internal network, so none of the three need a host port once the proxy is
the only thing in front of them. Add the proxy as its own service in the
same file:
# docker-compose.yml — add alongside the existing services.
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80" # ACME HTTP challenge
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
volumes:
caddy_data:Separate subdomains for the app and the API are the simplest correct routing — each proxies straight through, with no path-rewriting to get wrong:
retinue.example.com {
reverse_proxy frontend:3000
}
api.retinue.example.com {
reverse_proxy backend:8080
}with FRONTEND_ORIGIN=https://retinue.example.com,
BACKEND_URL=https://api.retinue.example.com, and
NEXT_PUBLIC_BACKEND_URL=https://api.retinue.example.com. That's the same
split used for the control-plane MCP endpoint; see the
app-vs-MCP host table for a worked
example with real hostnames. A single domain with the backend under a
path prefix (/api/*) works too, but the backend's own routes are
top-level (/personas, /jobs, /oauth/callback, …) — that needs the
prefix actually stripped before RetinueOS ever sees the request (Caddy's
handle_path,
not a bare reverse_proxy /api/*), and every RetinueOS URL that includes
the path (BACKEND_URL, NEXT_PUBLIC_BACKEND_URL, the OAuth redirect URI)
needs it too.
None of this changes any Compose identifier, route, or env var name —
see README → Self-hosting for exactly what
stays retinue regardless of the public RetinueOS display name.
Data and upgrades
Postgres data lives in the named volume retinue_postgres_data — back
that up like you would any database volume; there's no separate export
tool. Schema changes ship as Drizzle migrations, applied automatically on
backend startup. There's no published RetinueOS image — you build from
source — so upgrading is git pull followed by
docker compose up --build -d; there's no separate migration step to run
by hand.