Skip to content

Self Hosting

Archi edited this page Oct 20, 2026 · 2 revisions

Self-Hosting

Nautelle's control plane is four services: the API (NestJS), the web app (Next.js), PostgreSQL and Redis. It can run anywhere your runners can reach over HTTPS — runners dial out, so the machines that hold your code need no open port. There is no one-command production installer yet; this page describes the reference deployment Nautelle's own staging runs on, and every environment variable the services read.

flowchart LR
  cf["Cloudflare<br/>(Access + Tunnel)"] --> cfd["cloudflared"]
  cfd --> caddy["Caddy :80<br/>one origin"]
  caddy -->|"/api/* (prefix stripped,<br/>WebSockets included)"| api["API :8180"]
  caddy -->|"everything else"| web["Web :3517"]
  api --> pg[("PostgreSQL 16")]
  api --> redis[("Redis 7.4<br/>schedule timers")]
  api --- att[("attachments volume")]
  runner["nautelle-runner<br/>(your machines)"] -->|"wss://host/api/runner"| caddy
Loading

The reference deployment

The recipe lives in deploy/ and deploy/README.md:

  • Docker Compose runs PostgreSQL, Redis, the API, the web app, Caddy and cloudflared on a private network (docker-compose.yml). Nothing is published on the host.
  • Caddy gives everything one origin: /api/* goes to the API with the prefix stripped, WebSockets included, and everything else to the web app (Caddyfile). Cookies stay first-party. It also caps request bodies at 10 MB and sets security headers.
  • A Cloudflare Tunnel publishes that origin; Cloudflare Access can sit in front of it.
  • The API migrates and seeds in its entrypoint (prisma migrate deploy, then the idempotent seed) and refuses to start if either fails. Deploying is replacing containers.

1. Build the images

From the repository root:

docker build --pull -t agentdock-api -f apps/api/Dockerfile .
docker build --pull -t agentdock-web -f apps/web/Dockerfile \
  --build-arg NEXT_PUBLIC_LIVE_URL=wss://dock.example.com/api/live \
  --build-arg API_URL=http://api:8180 .

NEXT_PUBLIC_LIVE_URL and API_URL are baked into the web build; changing either means rebuilding the web image.

2. Configure

Copy the examples and fill them in. The reference stack uses .env for the compose passwords and env/api.env, env/cloudflared.env for the services (deploy/staging/env/). Generate random secrets on the host — never commit them:

openssl rand -base64 32   # APP_ENCRYPTION_KEY, LLM_SECRETS_KEY

Keep every secret file at mode 600.

3. Start

docker compose up -d

Check https://<host>/api/health.

4. Create the first admin

The seed creates no users. Run the admin CLI inside the API container:

docker exec -it <api-container> node dist/src/cli/admin-create.js

5. Pair a runner

In the web app, Admin › Runners › Add runner, then on each machine:

nautelle-runner pair --server https://<host>/api --code XXXX-XXXX --allow-root ~/dev

Behind Cloudflare Access, give the runner a service token with --access-client-id and --access-client-secret-stdin. Let the service token through on /api/runner and /api/runners/pair, and leave /api/hooks/* open — inbound webhooks and Claude Code hooks authenticate with their own signatures and tokens. See Installing the Runner.

Environment variables

API (apps/api/.env)

Variable Required Purpose
PORT yes API port, 8180
APP_ENV yes production marks session and CSRF cookies Secure
API_URL · WEB_URL yes public origins of the API and the web app (CORS uses WEB_URL)
APP_URL — public URL of the web app, for Telegram links and the Telegram Mini App (needs https)
TRUST_PROXY — whose X-Forwarded-For to believe for the client IP; loopback by default
DATABASE_URL yes PostgreSQL connection string
REDIS_URL yes¹ Redis for schedule timers
SCHEDULER_ENABLED — false runs no schedules worker and needs no Redis. Default true
QUEUE_PREFIX — key prefix of every queue. Default agentdock
APP_ENCRYPTION_KEY for integrations seals webhook secrets, the Telegram bot token and the GitHub App's secrets (32 bytes, base64)
LLM_SECRETS_KEY for AI features seals AI provider keys; without it the assistant does not start, the rest of the API runs
PUBLIC_URL for the GitHub App public HTTPS base GitHub can reach for POST /hooks/github
GITHUB_API_BASE — GitHub REST base. Default https://api.github.com
DEEPL_API_KEY — translate saved text with DeepL; empty uses a small model on the runner
SPEECH_TO_TEXT_URL · SPEECH_TO_TEXT_API_KEY · SPEECH_TO_TEXT_MODEL — a Whisper-compatible service for Telegram voice input. Model default whisper-1
ATTACHMENTS_DIR — where card attachments are stored. Default ./data/attachments
ATTACHMENTS_PROJECT_QUOTA_BYTES — storage per project. Default 1 GiB
APP_TIMEZONE — default time zone for new budgets. Default UTC
AUDIT_VERIFY_CRON — when the audit chain is verified. Default 0 3 * * *
USAGE_RECOMPUTE_BATCH — requests re-priced per transaction. Default 1000
ACTIVITY_POLL_MS · ACTIVITY_GAP_GRACE_MS · ACTIVITY_RETENTION_DAYS — activity projector tuning. Defaults 2000, 10000, 180
WEBHOOKS_WORKER_ENABLED — false keeps outbound webhook delivery off on this instance. Default true
TERMINAL_IDLE_TIMEOUT_SEC · TERMINAL_MAX_DURATION_SEC — terminal attach limits. Defaults 900, 14400
TELEGRAM_POLL_TIMEOUT_S · TELEGRAM_API_BASE · TELEGRAM_INIT_DATA_MAX_AGE_S — Telegram tuning. Defaults 30, https://api.telegram.org, 3600
ADMIN_EMAIL · ADMIN_PASSWORD — non-interactive admin:create; set, run, then remove

¹ Required unless SCHEDULER_ENABLED=false. The test-only variables (TEST_DATABASE_URL, TEST_REDIS_URL, BUN_BIN) are documented in the example file.

Web (apps/web/.env)

Variable Purpose
API_URL target of the same-origin /api rewrite; http://api:8180 inside Compose
NEXT_PUBLIC_LIVE_URL WebSocket URL of the API's live endpoint; wss://<host>/api/live behind the proxy

Docker (docker/.env, local development)

Variable Purpose
POSTGRES_PASSWORD must match DATABASE_URL
REDIS_PASSWORD must match REDIS_URL

The Telegram bot token and AI provider keys are not environment variables — an admin enters them under Admin › Integrations › Telegram and Admin › AI providers, and they are stored sealed.

Upgrading

  1. Pull the new version and rebuild both images.
  2. Replace the API container first and wait until it is healthy — it applies new migrations on start — then the web container. The reference refresh.sh does exactly this, and Caddy retries requests for up to 60 seconds meanwhile, so a deploy drops no requests.
  3. Upgrade the runners after the API.

Backups

Back up together:

  • the PostgreSQL volume — the source of truth;
  • the attachments volume — card attachments are not in the database.

Redis holds only schedule timers, which the API rebuilds from PostgreSQL. Keep APP_ENCRYPTION_KEY and LLM_SECRETS_KEY out of database backups: either alone is useless, both together are the secrets.

See also

Decisions: ADR-0007 · ADR-0015 · ADR-0016 · ADR-0031 · ADR-0059 · Spec: #368

Clone this wiki locally