RetinueOS

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_KEY and/or OPENAI_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
VariableRequiredWhat it's for
AUTH_PASSWORDYesThe one shared password gating the whole app (not multi-tenant).
ANTHROPIC_API_KEY / OPENAI_API_KEYAt least oneYour own LLM provider keys. Personas run on whichever model you assign.
BRAVE_SEARCH_API_KEYNoEnables the built-in web_search tool.
CREDENTIALS_ENCRYPTION_KEYOnly for authenticated MCP connectorsAES 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_URLFor OAuth connectorsThe backend's externally-reachable origin; builds the /oauth/callback redirect URI. Must exactly match what you register with the OAuth provider.
FRONTEND_ORIGINYes, if not on defaultsOrigin the backend allows via CORS — the browser-visible frontend URL.
NEXT_PUBLIC_BACKEND_URLYes, if not on defaultsPublic 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_URLNoWebhook destination for job/routine outcomes (e.g. an ntfy.sh topic).
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECTNo, all three togetherStandards-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-keys

Put 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 -d

Then, 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:

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.

On this page