Self-Hosting Configuration
Complete reference for Docker environment variables, Convex backend variables, service topology, and volume persistence.
This page is the complete configuration reference for a self-hosted Owlat instance. For step-by-step setup, see Self-Hosting.
Two Configuration Layers
Owlat uses two separate configuration layers. Mixing them up is the most common setup mistake.
- Docker
.env— read by Docker Compose at container startup. Controls ports, secrets shared between containers, and browser-facing URLs. - Convex environment variables — read by serverless functions inside the Convex backend. Set after deployment via
npx convex env set. Controls application behavior: email provider, auth secrets, sender identity, integrations.
Docker Environment Variables
These go in the .env file at the project root. Docker Compose reads them when starting containers.
Convex Backend
| Variable | Required | Default | Description |
|---|---|---|---|
INSTANCE_SECRET | Yes | — | Convex backend instance identity. Generate with openssl rand -hex 32. |
CONVEX_ADMIN_KEY | Yes | — | Admin key for deploying functions. Generated after first boot via docker compose exec convex ./generate_admin_key.sh. |
Public URLs
These must be reachable from the user's browser — do not use Docker-internal hostnames.
| Variable | Required | Default | Description |
|---|---|---|---|
NUXT_PUBLIC_CONVEX_URL | Yes | http://localhost:3210 | Convex backend URL (browser → Convex). |
NUXT_PUBLIC_CONVEX_SITE_URL | Yes | http://localhost:3211 | Convex site proxy URL (browser → HTTP actions). |
NUXT_PUBLIC_SITE_URL | Yes | http://localhost:3000 | Web application URL. |
MTA Configuration
The built-in MTA runs only when it is the delivery provider (EMAIL_PROVIDER=mta) or when postbox/inbox need it for inbound mail — gated by the mta Docker Compose profile. The default self-host ships COMPOSE_PROFILES=mta; a Resend/SES, receive-only, or IMAP-only deployment drops mta and doesn't run it. These variables apply only when the MTA is active. See Operating Modes.
| Variable | Required | Default | Description |
|---|---|---|---|
MTA_API_KEY | Yes | — | Shared secret for Convex → MTA authentication. Generate with openssl rand -base64 32. |
MTA_WEBHOOK_SECRET | Yes | — | HMAC secret for MTA → Convex webhook callbacks. Generate with openssl rand -base64 32. |
EHLO_HOSTNAME | For delivery | mail.localhost | SMTP EHLO/HELO hostname. The mail.localhost default boots fine; required for production sending, where it must match your server's rDNS PTR record. |
RETURN_PATH_DOMAIN | For delivery | bounces.localhost | Domain for VERP bounce return-path addresses. The bounces.localhost default boots fine; required for production sending, where it needs an MX record pointing to your server. |
IP_POOLS_TRANSACTIONAL | No | 127.0.0.1 | Comma-separated IPs for transactional email delivery. |
IP_POOLS_CAMPAIGN | No | 127.0.0.1 | Comma-separated IPs for campaign/marketing email delivery. |
DKIM_KEYS | No | {} | DKIM signing keys as JSON. See DNS & Email Setup. |
WORKER_CONCURRENCY | No | 50 | Number of parallel MTA worker threads. |
MTA_LOG_LEVEL | No | info | MTA log verbosity: debug, info, warn, error. |
Port Overrides
All ports can be changed if the defaults conflict with existing services.
| Variable | Default | Service |
|---|---|---|
CONVEX_PORT | 3210 | Convex backend API |
CONVEX_SITE_PORT | 3211 | Convex HTTP actions |
DASHBOARD_PORT | 6791 | Convex dashboard (requires --profile dashboard; bound to 127.0.0.1 only — reach via SSH tunnel) |
WEB_PORT | 3000 | Web application |
MTA_HTTP_PORT | 3100 | MTA HTTP API |
MTA_SMTP_PORT | 25 | MTA SMTP (bounce processing) |
The Convex, MTA HTTP, and dashboard ports default to localhost-only binding (127.0.0.1). CONVEX_BIND (default 127.0.0.1) controls the interface the Convex backend binds to, and REDIS_PASSWORD sets the Redis auth password.
Redis and ClamAV publish no host port — they are reached only over the internal Docker network (redis:6379, clamav:3310), so there is no REDIS_PORT/CLAMAV_PORT host-override knob. (CLAMAV_PORT is set on the MTA container as the internal address its scan client dials; it does not map a host port.)
Analytics (Optional)
| Variable | Default | Description |
|---|---|---|
NUXT_PUBLIC_POSTHOG_API_KEY | — | PostHog project API key for client-side tracking. |
NUXT_PUBLIC_POSTHOG_HOST | https://eu.i.posthog.com | PostHog instance URL. |
Deployment Mode
These control which UI the web app shows and whether in-app updates can run. The defaults match a self-hosted install — you rarely need to change them.
| Variable | Default | Description |
|---|---|---|
OWLAT_DEPLOYMENT_MODE | selfhost | Read by the web app (config.public.deploymentMode). Controls the self-host onboarding banner. Leave as selfhost for an OSS deploy. |
OWLAT_INSTALL_DIR | ./ | Absolute path to this repo on the host. The updater sidecar mounts it to write compose templates and run docker compose pull && up -d. Required for in-app updates. |
The owlat setup CLI writes an OWLAT_HOSTED_MODE value into .env, but no service in this OSS repo reads it — the hosted-cloud control plane (and its billing) was extracted to a separate private repo. Setting it has no effect on a self-hosted instance.
Image Pinning (Optional)
The compose file pins upstream images to minor versions via dedicated *_VERSION vars, each with a hard-coded default. Override only to pin a different version; production release compose files further lock these to @sha256 digests.
| Variable | Default | Image |
|---|---|---|
CONVEX_BACKEND_VERSION | latest | ghcr.io/get-convex/convex-backend |
CONVEX_DASHBOARD_VERSION | latest | ghcr.io/get-convex/convex-dashboard |
REDIS_VERSION | 7.4-alpine | redis |
CLAMAV_VERSION | stable | clamav/clamav |
CADDY_VERSION | 2.8-alpine | caddy (only used with --profile tls) |
Owlat's own images (web, mta, updater, convex-deploy, code-worker) are tagged with OWLAT_VERSION — the image pull tag defaults to latest, while the in-container build arg defaults to the dev local-build sentinel. The release pipeline writes the pinned tag, so do not edit OWLAT_VERSION, OWLAT_GIT_SHA, or OWLAT_BUILD_DATE by hand.
Convex Environment Variables
These are set after deploying functions. They control application-level behavior inside the Convex backend.
npx convex env set VAR_NAME "value" \
--url http://localhost:3210 \
--admin-key <your-admin-key>
Set These for Normal Operation
Set all of these on any real deployment. In code, most are read with sensible fallbacks and won't crash a function if missing — but auth redirects, unsubscribe links, and CORS will be wrong without them.
| Variable | Description |
|---|---|
SITE_URL | Public site URL for auth redirects (e.g., http://localhost:3000). Falls back to http://localhost:3000. |
BETTER_AUTH_SECRET | Secret for signing auth sessions. Generate with openssl rand -base64 32. |
UNSUBSCRIBE_SECRET | HMAC secret for signed unsubscribe tokens. Generate with openssl rand -base64 32. |
EMAIL_PROVIDER | Email provider: mta (default), ses, or resend. |
CONVEX_SITE_URL (used for tracking pixels and unsubscribe links) is a Convex built-in — convex env set rejects it with EnvVarNameForbidden. The backend derives it from CONVEX_SITE_ORIGIN on the convex container, which docker-compose interpolates from NUXT_PUBLIC_CONVEX_SITE_URL in your .env. Set the public URL there, not as a Convex function env var.
MTA_API_URL and MTA_API_KEY are the only variables that hard-throw — and only when an email is actually sent through the MTA provider (auth invites, campaigns, transactional). Sending fails with No system email transport configured if either is unset (apps/api/convex/systemMail.ts; the MTA provider itself lives in apps/api/convex/lib/sendProviders/mta/index.ts). Set both whenever EMAIL_PROVIDER=mta.
| Variable | Description |
|---|---|
MTA_API_URL | MTA service URL. Use http://mta:3100 for Docker networking. |
MTA_API_KEY | Must match the MTA_API_KEY in Docker .env. |
MTA_WEBHOOK_SECRET | Must match the MTA_WEBHOOK_SECRET in Docker .env. |
Sender Identity
| Variable | Default | Description |
|---|---|---|
DEFAULT_FROM_EMAIL | noreply@example.com | Default sender email address. |
DEFAULT_FROM_NAME | Owlat | Default sender display name. |
DEFAULT_FROM_DOMAIN | mail.owlat.app | Domain for system emails (invitations, etc.). |
ALLOWED_ORIGINS | — | Comma-separated CORS origins (e.g., http://localhost:3000,http://localhost:3001). |
LLM Provider (Optional)
Owlat talks to a single pluggable LLM provider (ADR-007). The same config powers AI agent replies, translations, knowledge-graph embeddings, and the visualization agent. Anything that speaks the OpenAI Chat Completions / Embeddings shape works (OpenAI, OpenRouter, Ollama, local vLLM/LM Studio). Only set these if you turn on an AI feature flag.
| Variable | Default | Description |
|---|---|---|
LLM_PROVIDER | openai | openai, openrouter, or ollama. Selects the client and a default base URL. |
LLM_API_KEY | — | Provider API key. Not required for ollama. |
LLM_BASE_URL | provider default | Override the endpoint (for self-hosted/local servers). OpenRouter and Ollama get sensible defaults automatically. |
LLM_MODEL_FAST | gpt-4o-mini | Model for fast tasks (classify/extract/guard/summarize). |
LLM_MODEL_CAPABLE | gpt-4o | Model for capable tasks (draft). |
LLM_EMBEDDING_MODEL | text-embedding-3-small | Embedding model for the knowledge graph. |
OPENAI_API_KEY and OPENROUTER_API_KEY are accepted as fall-back aliases for LLM_API_KEY (precedence: LLM_API_KEY → OPENROUTER_API_KEY → OPENAI_API_KEY). Prefer LLM_API_KEY on new installs (apps/api/convex/lib/llmProvider.ts).
LLM_PROVIDER accepts only openai, openrouter, and ollama. To run Claude models, use the OpenAI-compatible path: set LLM_PROVIDER=openai (or leave the default), point LLM_BASE_URL at an OpenAI-compatible endpoint, and set LLM_MODEL_CAPABLE / LLM_MODEL_FAST to the Claude model IDs.
Other Integrations (Optional)
| Variable | Description |
|---|---|
GOOGLE_SAFE_BROWSING_API_KEY | Google Safe Browsing API v4 key for URL reputation checking. |
MTA_INTERNAL_URL | Preferred in-cluster MTA base URL used for all Convex→MTA HTTP calls (outbound send, cache push, delivery hooks, attachment scan); overrides the public MTA_API_URL. e.g. http://mta:3100. |
MAIL_SYNC_API_URL | Mail-sync worker URL for external IMAP/SMTP accounts. Without it, an outbound external send is marked failed (errorCode EXTERNAL_NOT_CONFIGURED) on every recipient rather than dispatched. |
MAIL_SYNC_API_KEY | Shared secret for the mail-sync worker. |
POSTHOG_API_KEY | PostHog API key for server-side analytics. |
POSTHOG_HOST | PostHog instance URL (default: https://eu.i.posthog.com). |
For the complete variable reference including AWS SES, Resend, and inbound-channel webhooks, see Environment Variables.
Service Topology
| Service | Ports | Depends On | Healthcheck |
|---|---|---|---|
| convex | 3210 (API), 3211 (site proxy) | — | curl -f http://localhost:3210/version every 15s |
| convex-dashboard | 6791 (127.0.0.1 only) | convex (healthy) | — (dashboard profile; reach via SSH tunnel ssh -L 6791:127.0.0.1:6791 host) |
| web | 3000 | convex (healthy) | — |
| mta | 3100 (HTTP), 25 (SMTP) | redis (healthy), convex (healthy), clamav (started, optional) | — |
| redis | 6379 | — | redis-cli ping every 10s |
| clamav | 3310 | — | clamdcheck every 60s (600s start delay) (clamav profile; off unless file scanning is enabled) |
| updater | 3200 (internal only) | docker-socket-proxy | — |
| convex-deploy | — | convex (healthy) | One-shot (deploy profile) |
| caddy | 80, 443 | web, convex, mta | — (tls profile) |
| code-worker | — (internal only) | convex (healthy) | — (inbox-codetasks profile; also dev) |
| imap | 993 | convex (healthy), imap-cert-init (completed) | — (personal-mail profile) |
| mail-sync | — (internal only) | convex (healthy) | — (external-mail profile) |
| ollama | — (internal only) | — | — (ai / dev profiles) |
The updater sidecar has no public port — the web app reaches it at http://updater:3200 over the Docker network. It mounts only OWLAT_INSTALL_DIR read-write so it can write a new compose template, and reaches the Docker API through the least-privilege docker-socket-proxy sidecar (which mounts the socket read-only) via DOCKER_HOST=tcp://docker-socket-proxy:2375 — it no longer mounts the Docker socket directly.
The optional caddy service (--profile tls) is a reverse proxy that terminates TLS via Let's Encrypt and fronts web, convex, and mta on subdomains. Edit Caddyfile to match your domains before bringing it up, or front the stack with your own reverse proxy instead.
The code-worker sidecar (--profile inbox-codetasks, also available via --profile dev) is the AI coding agent that picks up code tasks. It reads the LLM_* and GITHUB_* variables and is off by default.
Volumes
| Volume | Persists | Backup Priority |
|---|---|---|
convex-data | Database, file storage, vector indexes | Critical — all application data |
redis-data | MTA job queue (AOF) | Medium — in-flight email jobs |
clamav-data | Virus definition signatures | Low — re-downloads automatically |
caddy-data / caddy-config | TLS certificates and Caddy state (only with --profile tls) | Low — certificates re-issue automatically |
code-workspace | Code-worker checkout (only with --profile inbox-codetasks or --profile dev) | Low — ephemeral working tree |
mail-certs | IMAP server TLS certificates (only with --profile personal-mail) | Low — certificates re-issue automatically |
ollama-data | Ollama model files (only with --profile ai / --profile dev) | Low — models re-download automatically |