Deployment & networking

Loma’s Docker Compose stack includes a bundled nginx reverse proxy as the single external entrypoint. The backend (:3000) and dashboard (:3001) are bound to 127.0.0.1 only, so you expose just one port and the backend never touches the public internet.

client ──▶ nginx ──┬─ /api/auth/* , /api/signup ─▶ dashboard (sign-in)
                   ├─ /api/*                    ─▶ backend  (identity injected by the proxy)
                   ├─ /webhooks/* , /webhook    ─▶ backend  (third-party webhooks)
                   └─ everything else           ─▶ dashboard (UI)

For every /api/* call the proxy makes an internal auth_request to the dashboard to resolve the signed-in user and inject a trusted identity header before forwarding to the backend — so the backend always knows the caller’s role, and a client can’t spoof it.

HTTP on an IP (default)

Out of the box the proxy serves HTTP on :80 (server_name _) — works locally and on a bare IP, no domain needed. Open inbound 80 in your firewall/security group (leave 3000/3001 closed), set:

  • .env: PUBLIC_BASE_URL=http://<your-ip>
  • dashboard/.env: AUTH_URL=http://<your-ip>

then docker compose up -d and open http://<your-ip>.

HTTPS on :443 (a few steps)

HTTPS is opt-in via the committed docker-compose.tls.yml overlay, which renders an nginx TLS config for your domain from ${LOMA_DOMAIN} — no hardcoded domain, and the repo default stays HTTP. Prereqs: a DNS A record for your domain → the server’s (ideally static/Elastic) IP, and inbound 443 open.

  1. Get a certificate while the default HTTP stack is up (it already serves the ACME challenge on :80):

    docker compose run --rm certbot certonly --webroot -w /var/www/certbot \
      -d your-domain.com --email you@example.com --agree-tos --no-eff-email
  2. Enable HTTPS — in .env:

    LOMA_DOMAIN=your-domain.com
    COMPOSE_FILE=docker-compose.yml:docker-compose.tls.yml
    PUBLIC_BASE_URL=https://your-domain.com

    and in dashboard/.env: AUTH_URL=https://your-domain.com.

  3. docker compose up -d — nginx now serves :443 and redirects :80 → :443. Renew with a cron that runs certbot renew and reloads nginx.

LOMA_DOMAIN/COMPOSE_FILE live in your untracked .env and certs in an untracked volume, so HTTPS survives a git pull redeploy. Full runbook: deploy/nginx/README.md in the repo.

Behind a CDN (e.g. Cloudflare)

If your domain is proxied through a CDN, TLS is terminated at the edge. Secure the CDN→origin hop with an origin certificate (e.g. Cloudflare’s, with Full (Strict)), restrict the origin’s :80/:443 to the CDN’s IP ranges (or enable authenticated origin pulls), and keep X-Forwarded-Proto: https flowing (the proxy already sets it).

Configuration behind the proxy

  • Set PUBLIC_BASE_URL (backend .env) and AUTH_URL (dashboard/.env) to the public origin (http://<ip> or https://your-domain.com).
  • Keep BACKEND_URL=http://loma-backend:3000 — the internal address the dashboard proxies to.
  • Provider webhooks should point at https://your-domain.com/webhooks/... (Slack uses Socket Mode and is unaffected).
  • A domain + HTTPS is also what unlocks Google sign-in — see Authentication.

Backups

Back up all three stores:

  • MongoDB — conversations, skills, flows, users, integrations, usage.
  • LOMA_SKILL_ASSET_DIR — non-text skill assets.
  • The Claude-users volume — connected Claude account credentials for the pool.
  • Your TLS certs (deploy/nginx/certbot/conf) if you’re not regenerating them.