# Deployment

The doctate-server runs in two flavors that can coexist:

- **Local dev** — `cargo run -p doctate-server` (or `scripts/run-server.sh`),
  reads `server/.env` + `server/settings.toml` + `server/users.toml` from
  the repo working tree, writes to `tmpdata/`.
- **Production on minerva** — Docker container, reads operator-owned
  configs from `/opt/stacks/doctate-server/config/`, writes to
  `/opt/stacks/doctate-server/data/`.

Both have independent configs, independent data directories, and
listen on different hosts — no interference.

## Architecture

```mermaid
graph LR
    Dev[Dev laptop<br/>scripts/deploy-server.sh] -->|ssh + scp| Host

    subgraph Host[minerva.lan]
        Stack["/opt/stacks/doctate-server/"]
        Container["doctate-server container<br/>network_mode: host<br/>:3000"]

        Stack -->|bind-mount :ro| Configs[("config/.env<br/>config/settings.toml<br/>config/users.toml")]
        Stack -->|bind-mount :rw| Data[("data/<br/>logs/")]

        Configs -.->|read on boot| Container
        Container -.->|writes recordings + logs| Data
    end

    Host -->|http :3000| nginx[nginx on Unraid<br/>TLS termination]
    nginx -->|https| Browser
```

## First deploy

The script handles bootstrap idempotently. The first run will fail at
phase 3 (placeholder guard) so you can fill in the configs.

1. **Run the script** from the dev laptop:

   ```bash
   ./scripts/deploy-server.sh
   ```

   Phases 1–2 create `/opt/stacks/doctate-server/{config,data,logs}` and
   seed the three config files from `server/*.example`. Phase 3 then
   aborts with placeholder errors.

2. **Edit configs on minerva** (via `nano` over SSH or through Dockge).
   Required fields:

   | File | Edit |
   |---|---|
   | `config/users.toml` | At least one `[[user]]` block with a real `api_key` and a real bcrypt hash for `web_password` |
   | `config/settings.toml` | `[whisper] url`, `[ollama] url` for the production hosts |
   | `config/.env` | Optionally `IONOS_API_KEY=…` if any LLM backend uses Ionos |

   Generate the bcrypt hash inside the container — the image is already
   loaded on minerva after phase 5 of run 1:

   ```bash
   ssh minerva.lan 'docker run --rm -i doctate-server:latest hash-password'
   ```

   Copy the resulting `$2b$12$…` line into `users.toml`.

3. **Re-run the script**:

   ```bash
   ./scripts/deploy-server.sh
   ```

   This time it runs all phases, ending with a green health-check and a
   rollback hint.

## Re-deploy

Once the configs are in place, every subsequent deploy is one command:

```bash
./scripts/deploy-server.sh
```

The script is idempotent. Phase 1 (`mkdir -p`) and phase 2 (template
seeding) are no-ops on a populated host. Phase 3 (placeholder guard)
passes. Phases 4–9 build, transfer, restart, and verify health.

The image is tagged twice on minerva: as `doctate-server:latest` and
as `doctate-server:<git-short-sha>`. The script keeps the **last 5
SHA-tagged builds** automatically (phase 9) and prunes older ones plus
their dangling layers, so disk usage stays bounded even after many
deploys. Adjust `KEEP=5` near the bottom of `scripts/deploy-server.sh`
if you want a different rollback window.

## User management

Adding or rotating a user means editing `users.toml` on minerva and
restarting the container so the boot-time parser picks up the change:

```bash
ssh minerva.lan 'docker run --rm -i doctate-server:latest hash-password'
ssh minerva.lan 'nano /opt/stacks/doctate-server/config/users.toml'
./scripts/restart-server.sh
```

`scripts/restart-server.sh` wraps the `docker compose ... restart` plus
a `/api/health` poll, so you see in one line whether the new config is
actually live or whether the server hit a boot-time error.

The `users.toml` mount is read-only by design — the container is not
allowed to modify the auth file. Edits happen on the host.

## Rollback

Each deploy leaves a `doctate-server:<sha>` tag behind on minerva, so
rolling back to a previous build is one retag plus a restart:

```bash
ssh minerva.lan
docker images doctate-server                              # list tags + dates
docker tag doctate-server:<previous-sha> doctate-server:latest
cd /opt/stacks/doctate-server && docker compose up -d --force-recreate
```

Persistent data (`/opt/stacks/doctate-server/data/`) is unaffected by
rollback — only the binary swaps.

## Troubleshooting

| Symptom | First check |
|---|---|
| Container restarts in a loop | `ssh minerva.lan 'docker logs --tail 80 doctate-server'` — most boot panics name the missing env-var |
| `/api/health` doesn't respond | `ssh minerva.lan 'ss -tlnp \| grep :3000'` — is the server actually bound? |
| Login refused on HTTPS | `COOKIE_SECURE` and TLS-proxy mismatch — see "TLS" section below |
| File missing inside container | `docker compose -f /opt/stacks/doctate-server/compose.yaml exec doctate-server ls /app` |
| Health-check status | `ssh minerva.lan 'docker inspect --format "{{.State.Health.Status}}" doctate-server'` |
| Config edit not picked up | `./scripts/restart-server.sh` — most config is read on boot, not per request |
| Stale image tag on minerva | `docker images --filter dangling=true` — retag or `docker image prune` |

## TLS / nginx reverse proxy

The container ships with `COOKIE_SECURE=false` because the default
deployment is plain HTTP between minerva:3000 and whatever sits in
front of it. When nginx on the Unraid box terminates TLS and forwards
to `http://minerva.lan:3000`, set the cookie flag accordingly so
browsers actually accept the session cookie:

```bash
ssh minerva.lan "echo 'COOKIE_SECURE=true' >> /opt/stacks/doctate-server/config/.env"
./scripts/restart-server.sh
```

Without this override under a TLS proxy, the login flow looks like it
works (`POST /api/login` returns 200) but the next request lacks the
cookie and bounces back to the login page.
