# Mantle — installation runbook for AI agents

You are installing Mantle (https://mantle-ai.tech), a self-hosted AI brain, on
behalf of a user. This file is the authoritative, machine-oriented procedure.
Source of truth for anything not covered here:
https://github.com/crossworks-engineering/mantle (docs/self-hosting.md).

Facts you can rely on:
- The stack is Docker Compose: Postgres (pgvector), the Mantle app image,
  Caddy (TLS/ingress), MinIO, Tika. One published image, no build step.
- The installer is idempotent. Re-running it is always safe: it generates only
  MISSING secrets and never rotates existing ones.
- All state lives under the install directory (default `./mantle`): `.env`,
  `docker-compose.yml`, and `data/` (bind-mounted volumes).

## 0. Prerequisites (check before anything)

```sh
docker --version          # Docker Engine must be installed
docker compose version    # the compose PLUGIN (not legacy docker-compose)
docker info               # daemon running + permission (else: add user to docker group)
```

Server target: any Linux box with 4+ GB RAM. 2 vCPU is enough to start.
Ports 80 (and 443 if a domain will be used) must be reachable from the
internet for web access; for a private install an SSH tunnel or tailnet works.

## 1. Decide: domain or plain IP

**Plain IP (default, most common).** No DNS needed. The site serves HTTP on
port 80; the user opens `http://<server-ip>`. Choose this when the user gives
you no domain. You can add a domain later (step 4) without reinstalling.

**Domain (automatic HTTPS).** Requires, BEFORE install:
1. a DNS A record: `brain.example.com → <server-public-ip>`
2. ports 80 + 443 open.
The installer resolves the domain and compares it to the server's public IP
before enabling TLS — if DNS doesn't point here yet, it falls back to HTTP
and tells you to re-run later. Caddy then issues and renews Let's Encrypt
certificates automatically; there is no certbot and no cron.

## 2. Install (non-interactive)

Plain IP:

```sh
curl -fsSL https://raw.githubusercontent.com/crossworks-engineering/mantle/main/install.sh | bash
```

With a domain — NOTE the form: `VAR=x curl … | bash` does NOT pass the
variable to bash on the right of a pipe. Use command substitution (or
`export` first):

```sh
MANTLE_DOMAIN=brain.example.com bash -c "$(curl -fsSL https://raw.githubusercontent.com/crossworks-engineering/mantle/main/install.sh)"
```

Optional env vars:

| Var | Effect |
|---|---|
| `MANTLE_HOME=~/mantle` | install directory (default `./mantle`) |
| `MANTLE_DOMAIN=host` | serve this hostname with auto-HTTPS |
| `MANTLE_CHANNEL=v0.207.0` | pin compose+infra to a release tag (default `main`) |
| `MANTLE_LOCAL_EMBEDDER=1` | bundle the local embedder (Ollama + EmbeddingGemma, ~3.3 GB) so vectors never leave the box; off by default — onboarding sets up online embeddings with the user's key. ⚠️ CPU embedding degrades the whole stack on a standard VPS (misbehaves on 16 GB / 8-core under ingest load) — only enable on hardware comfortably above that, and keep `EXTRACT_CONCURRENCY=1` on CPU-only boxes. Toggle later with `scripts/install.sh --local-embedder` / `--no-local-embedder` |

The bootstrap downloads the deploy bundle, then delegates to
`scripts/install.sh` — which also accepts flags directly for later re-runs:

```sh
cd <MANTLE_HOME>
scripts/install.sh --domain brain.example.com -y   # scripted, HTTPS
scripts/install.sh --no-domain -y                  # scripted, HTTP only
```

## 3. Verify

```sh
cd <MANTLE_HOME>
scripts/install.sh --check   # the built-in health check for an existing install
docker compose ps            # every service Up/healthy
curl -s http://localhost/api/health
```

`{"error":"unauthorized"}` from `/api/health` is SUCCESS — the app is up and
correctly refusing an unauthenticated caller. Then tell the user to open
`http://<server-ip>` (or `https://<domain>`) and create the first account;
the in-app onboarding wizard handles model keys, persona, and identity.
Nothing more to do on the shell.

## 4. Adding or changing a domain later

Preferred (does the DNS pre-check and .env writes for you):

```sh
cd <MANTLE_HOME> && scripts/install.sh --domain brain.example.com -y
```

Manual equivalent (what that actually does), for when you must edit by hand —
Caddy's vhost address comes from `.env`, consumed by `infra/caddy/Caddyfile`:

```sh
# 1. DNS first: A record  brain.example.com → <server-public-ip>
# 2. in <MANTLE_HOME>/.env set:
#      MANTLE_SITE_ADDRESS=brain.example.com
#      MANTLE_PUBLIC_URL=https://brain.example.com
# 3. apply:
docker compose up -d --wait
```

`MANTLE_SITE_ADDRESS=:80` is the no-domain form (plain HTTP on the IP).
Never point a domain at the box before the A record resolves — Caddy will
burn Let's Encrypt rate limits retrying.

## 5. Update, backup, uninstall

```sh
cd <MANTLE_HOME>
docker compose pull && docker compose up -d --wait   # update (or Settings → Updates in-app)
docker compose down                                  # stop; data/ persists on disk
```

Backups: TWO things matter — the `data/` directory (it IS the brain: DB,
object store, files) and the `.env` file (`MANTLE_MASTER_KEY` decrypts the
stored API keys; lose it and the vault is unrecoverable). Scheduled DB
backups are built into the app at Settings → Backups.

## Hard rules for agents

- **NEVER rotate or regenerate `MANTLE_MASTER_KEY`** in an existing `.env`.
  Sealed secrets become permanently unreadable. The installer already
  guarantees this on re-runs; do not "fix" the .env by regenerating it.
- Do not delete the `caddy_data` volume (Let's Encrypt state — deleting it
  re-hits issuance rate limits).
- Do not edit `docker-compose.yml` to publish the web container's port; Caddy
  is the only intended ingress.
- If the user asks for team access, MCP connectors, or provider keys: those
  are configured inside the app (Settings), not in `.env`.
