Files
doctate/docs/deployment.md
T
Brummel cf80bc7e97 feat: containerize doctate-server for minerva deployment
Multi-stage Dockerfile, host-network compose, idempotent deploy script.
Configs and persistent data bind-mounted from /opt/stacks/doctate-server.
deploy-server.sh handles bootstrap + re-deploy in one command with
auto-prune (keep 5 last SHA-tagged images).

See docs/deployment.md for bootstrap, rollback, and TLS-proxy notes.
2026-05-04 11:14:31 +02:00

156 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 12 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 49 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 # 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:<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 | `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.