# 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
scripts/deploy-server.sh] -->|ssh + scp| Host
subgraph Host[minerva.lan]
Stack["/opt/stacks/doctate-server/"]
Container["doctate-server container
network_mode: host
:3000"]
Stack -->|bind-mount :ro| Configs[("config/.env
config/settings.toml
config/users.toml")]
Stack -->|bind-mount :rw| Data[("data/
logs/")]
Configs -.->|read on boot| Container
Container -.->|writes recordings + logs| Data
end
Host -->|http :3000| nginx[nginx on Unraid
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:`. 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 # silent prompt, prints hash
nano /opt/stacks/doctate-server/config/users.toml # paste the hash
docker compose -f /opt/stacks/doctate-server/compose.yaml restart doctate-server
```
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:` 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: 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 | `docker compose ... restart doctate-server` — 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
docker compose -f /opt/stacks/doctate-server/compose.yaml restart doctate-server
```
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.