Self-Hosting

Deploy Owlat on your own infrastructure with Docker Compose. Complete guide from first boot to production.

Owlat runs entirely on your own infrastructure via a single Docker Compose file. No cloud services are required — the stack includes its own database, real-time engine, mail server, and antivirus scanner.

Why Self-Host

  • Data sovereignty — your emails, contacts, and analytics never leave your servers
  • Compliance — meet GDPR, HIPAA, or industry-specific data residency requirements
  • Air-gapped environments — run without any external network dependencies
  • Full control — customize, audit, and extend every component

Prerequisites

RequirementMinimum
Docker20.10+
Docker Composev2 (the docker compose plugin)
RAM4 GB (8 GB recommended)
Disk20 GB free
opensslAny version (for generating secrets)
Domain + DNS controlRequired for production email delivery

Fastest path

On a fresh Linux VPS with Docker + Docker Compose v2 installed:

curl -fsSL https://get.owlat.app | bash

The one-liner clones the repo, runs preflight checks, and hands off to the interactive setup wizard described below. Non-interactive usage:

# Skip prompts, accept all defaults
OWLAT_ASSUME_YES=1 curl -fsSL https://get.owlat.app | bash

# Read answers from a config file (for CI / Ansible)
OWLAT_CONFIG_FILE=/path/to/answers.env curl -fsSL https://get.owlat.app | bash
Not included in self-hosted

Stripe billing, Hetzner VPS provisioning, and tier management live in the hosted-cloud control plane (apps/nest + apps/nest-api) and are disabled in self-hosted mode. Run with docker compose up -d — the control plane is gated behind --profile hosted and only operators of the managed service need it.

Quick Start

The interactive wizard generates secrets, writes your .env, starts the Docker stack, deploys functions, and creates your admin account — all automatically.

git clone https://github.com/owlat/owlat.git
cd owlat
bash scripts/setup.sh
# Select "Self-Hosted (Docker Compose)" when prompted

The wizard takes about 5 minutes and handles everything below.

Option B: Manual Setup

1. Clone and configure

git clone https://github.com/owlat/owlat.git
cd owlat
cp .env.selfhost.example .env

2. Generate secrets

# Instance secret (hex)
openssl rand -hex 32

# MTA API key and webhook secret (base64)
openssl rand -base64 32
openssl rand -base64 32

Paste the generated values into .env for INSTANCE_SECRET, MTA_API_KEY, and MTA_WEBHOOK_SECRET.

3. Start the stack

docker compose up -d

4. Wait for Convex to be ready

# Watch logs until you see "ready" or "listening"
docker compose logs -f convex
# Press Ctrl+C once ready

5. Generate the admin key

docker compose exec convex ./generate_admin_key.sh

Copy the output key and add it to your .env:

# In .env, set:
CONVEX_ADMIN_KEY=<paste-key-here>

Restart to pick up the new key:

docker compose up -d

6. Deploy functions

# Deploy the main Convex functions
docker compose --profile deploy run --rm convex-deploy

# Deploy the control plane functions
docker compose --profile deploy run --rm nest-api-deploy

7. Set Convex environment variables

These are application-level variables read by the serverless functions. Set them using the Convex CLI with your admin key:

export CONVEX_URL=http://localhost:3210
export CONVEX_ADMIN_KEY=<your-admin-key>

npx convex env set BETTER_AUTH_SECRET "$(openssl rand -base64 32)" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set UNSUBSCRIBE_SECRET "$(openssl rand -base64 32)" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set SITE_URL "http://localhost:3000" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set CONVEX_SITE_URL "http://localhost:3211" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set EMAIL_PROVIDER "mta" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set MTA_API_URL "http://mta:3100" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set MTA_API_KEY "<your-mta-api-key>" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set MTA_WEBHOOK_SECRET "<your-mta-webhook-secret>" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set DEFAULT_FROM_EMAIL "noreply@example.com" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set DEFAULT_FROM_NAME "Owlat" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY
npx convex env set DEFAULT_FROM_DOMAIN "example.com" --url $CONVEX_URL --admin-key $CONVEX_ADMIN_KEY

8. Open the dashboard

open http://localhost:3000
Admin account

The setup wizard creates an admin account automatically. For manual setup, use the Convex dashboard at http://localhost:6791 to create your first user, or call the seed endpoint documented in the setup script.

Stack Overview

ServicePort(s)Role
Convex3210, 3211Database, real-time subscriptions, serverless functions, file storage
Convex Dashboard6791Admin UI for inspecting data and debugging functions
Web3000Nuxt application — dashboard, email builder, settings
MTA3100, 25Mail transfer agent — SMTP delivery, bounce processing, IP warming
Redis6379Job queue and rate limiting state for the MTA
ClamAV3310Antivirus scanning for email attachments
Nest3001Admin panel for organization and infrastructure management

For a deeper dive into the architecture, see Self-Hosting Architecture.

What's Next