# 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.