Maintenance & Updates

Keep your self-hosted Owlat instance up to date, manage backups, scale performance, and troubleshoot common issues.

This guide covers day-to-day maintenance of your self-hosted Owlat instance. For initial setup, see Self-Hosting.

Updating

Owlat supports three ways to update: in-app, CLI, or manual. All three do the same thing under the hood — pull the pinned compose template for a tagged release, apply it, and redeploy Convex functions.

Platform admins see an "Update available" notification at Settings → System & Updates when a new version is released. Click Update now, confirm, and the web app will:

  1. Download the pinned docker-compose-<version>.yml from GitHub Releases
  2. Dispatch it to the updater sidecar
  3. Pull new images, recreate containers, redeploy Convex functions
  4. Verify the new version is live

The page polls container health and auto-reloads once the update is complete. Total time: typically 2–5 minutes.

Option B: owlat upgrade CLI

Installed to /usr/local/bin/owlat by the installer:

owlat upgrade                     # to latest
owlat upgrade --version 1.2.3     # to a specific version (useful for rollback)

The CLI drives the same updater sidecar the in-app flow uses.

Option C: Manual

For air-gapped environments or if you prefer to drive updates yourself:

# 1. Pull the pinned compose template for the target version
curl -fsSL https://github.com/owlat/owlat/releases/download/v1.2.3/docker-compose-1.2.3.yml \
  -o docker-compose.yml

# 2. Pull the new images
docker compose pull

# 3. Recreate containers
docker compose up -d

# 4. Re-deploy Convex tenant functions
docker compose --profile deploy run --rm convex-deploy

# 5. (Hosted-cloud operators only) Re-deploy control-plane functions
docker compose --profile hosted --profile deploy run --rm nest-api-deploy
Always re-deploy functions

Pulling new Docker images updates the containers, but the Convex serverless functions are deployed separately. The in-app and CLI paths run this for you; the manual path requires step 4 above.

Recovering from a failed update

If an update fails mid-flight, your stack may be in a mixed state — some containers on the new version, some on the old. Your data is not at risk: Convex's LMDB storage and Redis's AOF log are both crash-safe, and the updater never touches the volumes.

Diagnose first

# What's running?
owlat status                       # or: docker compose ps

# What did each container log?
owlat logs web                     # or: docker compose logs --tail=200 web
owlat logs mta
owlat logs convex
owlat logs updater

# Environment health check
owlat doctor                       # checks DNS, SMTP egress, secrets, containers

Rollback to the previous version

Every tagged release attaches its docker-compose-<version>.yml to the GitHub Release page. To roll back:

Via CLI:

# Replace with the last-known-good version
owlat upgrade --version 1.2.2

Manually:

# Download the previous release's compose file
curl -fsSL https://github.com/owlat/owlat/releases/download/v1.2.2/docker-compose-1.2.2.yml \
  -o docker-compose.yml

# Pull the pinned images and recreate
docker compose pull
docker compose up -d

# Re-deploy functions at the old version
docker compose --profile deploy run --rm convex-deploy

Docker will pull images for the pinned tags (e.g. ghcr.io/owlat/web:1.2.2) and swap containers in-place.

If the web container won't start

# See why
docker compose logs web

# Rule out a stale image
docker compose pull web
docker compose up -d --force-recreate web

If Convex won't start

Convex is the only stateful service in the stack besides Redis and ClamAV. If it's failing to start, check disk space on the convex-data volume first:

docker system df -v | grep convex-data

If disk is full, free space or resize the underlying volume. Never delete the convex-data volume — that's your database.

Nuclear option: restore from backup

If the stack is in an unrecoverable state, restore the most recent scripts/backup.sh archive:

bash scripts/restore.sh backups/owlat-YYYYMMDD-HHMMSS.tar.gz

See the Backups section below for how to set up automated backups.

Reporting an update bug

If an update fails in a way you think is a bug in Owlat itself (rather than your environment), capture and attach to a GitHub issue:

owlat doctor                                   > /tmp/owlat-doctor.txt 2>&1
docker compose ps                              > /tmp/owlat-ps.txt
docker compose logs --tail=500                 > /tmp/owlat-logs.txt 2>&1
cat /opt/owlat/docker-compose.yml              > /tmp/owlat-compose.yml

File at: https://github.com/owlat/owlat/issues/new

Automating Updates

Create a systemd timer or cron job for automatic updates:

# /etc/cron.d/owlat-update
0 4 * * * root cd /opt/owlat && docker compose pull && docker compose up -d && docker compose --profile deploy run --rm convex-deploy && docker compose --profile deploy run --rm nest-api-deploy
Update safety

Convex handles database schema migrations automatically. Pulling new images and re-deploying functions is safe — the backend manages schema changes during deployment.

ClamAV Signatures

The ClamAV container runs freshclamd automatically, which downloads updated virus definitions daily. No manual intervention is needed.

To force an immediate signature update:

docker compose exec clamav freshclam

To check the current signature version:

docker compose exec clamav clamscan --version

Redis Maintenance

Redis is configured with AOF (Append Only File) persistence by default. This ensures the MTA job queue survives container restarts.

  • Compaction — Redis automatically rewrites the AOF file to keep it compact.
  • Memory — monitor Redis memory usage with docker compose exec redis redis-cli info memory.
  • Flushing — if you need to clear the queue (e.g., after a misconfiguration): docker compose exec redis redis-cli FLUSHALL. This discards all in-flight email jobs.

Scaling

MTA Throughput

Increase WORKER_CONCURRENCY in your .env to process more email groups in parallel:

# Default: 50 workers
WORKER_CONCURRENCY=100

Restart the MTA to apply: docker compose restart mta

Server Sizing

LoadvCPURAMDisk
Up to 10K contacts24 GB40 GB
Up to 100K contacts48 GB80 GB
100K+ contacts816 GB160 GB

Convex Backend

Convex is a single-node service that scales vertically. Monitor disk usage on the convex-data volume — this is where all database records, file uploads, and vector indexes are stored.

# Check volume disk usage
docker system df -v | grep convex-data

Troubleshooting

Convex won't start

docker compose logs convex

Common causes:

  • INSTANCE_SECRET not set — check your .env file has a valid hex string
  • Disk full — the convex-data volume needs free space for the database
  • Port conflict — another service is using port 3210 or 3211

MTA can't send emails

docker compose logs mta

Common causes:

  • EHLO hostname doesn't match PTR record — receiving servers reject the connection. Verify with dig -x YOUR_IP +short
  • DKIM_KEYS JSON is invalid — validate the JSON: echo $DKIM_KEYS | jq .
  • Port 25 blocked — many cloud providers (AWS, GCP, Azure) block outbound SMTP by default. Request port 25 access or use an email relay
  • MTA_API_KEY mismatch — the key in Docker .env must match the one set in Convex env vars

ClamAV is slow to start

This is normal. ClamAV loads virus definitions into memory on startup, which takes 1-2 minutes. The Docker healthcheck has a start_period: 120s to account for this.

The MTA waits for ClamAV to be healthy before starting (configured via depends_on in Docker Compose).

Web UI shows connection error

The browser needs to reach the Convex backend directly. If NUXT_PUBLIC_CONVEX_URL uses a Docker-internal hostname (like http://convex:3210), the browser can't connect.

Fix: set NUXT_PUBLIC_CONVEX_URL to a URL reachable from the browser:

  • Local dev: http://localhost:3210
  • Production: https://convex.example.com (via reverse proxy)

Function deployment fails

docker compose --profile deploy run --rm convex-deploy 2>&1

Common causes:

  • CONVEX_ADMIN_KEY is wrong or missing — regenerate: docker compose exec convex ./generate_admin_key.sh
  • Convex container not healthy — check with docker compose ps
  • Schema conflict — if you modified Convex schema files, check for validation errors in the deploy output

Migrating to Production

To move from a local development setup to a production deployment:

  1. Update URLs in .env:
    NUXT_PUBLIC_CONVEX_URL=https://convex.example.com
    NUXT_PUBLIC_CONVEX_SITE_URL=https://convex-site.example.com
    NUXT_PUBLIC_SITE_URL=https://owlat.example.com
    
  2. Set up DNS — configure A records, PTR, SPF, DKIM, and DMARC. See DNS & Email Setup.
  3. Add a reverse proxy — Caddy or Nginx with TLS. See Production Deployment.
  4. Secure Redis — add REDIS_PASSWORD as described in Production Deployment.
  5. Update Convex env vars to match the new URLs:
    npx convex env set SITE_URL "https://owlat.example.com" --url http://localhost:3210 --admin-key <key>
    npx convex env set CONVEX_SITE_URL "https://convex-site.example.com" --url http://localhost:3210 --admin-key <key>
    npx convex env set ALLOWED_ORIGINS "https://owlat.example.com" --url http://localhost:3210 --admin-key <key>
    
  6. Restart the stack: docker compose up -d
  7. Verify — send a test email and check delivery with mail-tester.com.