One-line wrapper around docker compose ... restart plus a /api/health poll, useful after editing users.toml/settings.toml/.env on minerva (boot-time read). docs/deployment.md updated (user-mgmt, troubleshooting, TLS-proxy sections) to reference the wrapper instead of the inline ssh+docker command.
5.7 KiB
Deployment
The doctate-server runs in two flavors that can coexist:
- Local dev —
cargo run -p doctate-server(orscripts/run-server.sh), readsserver/.env+server/settings.toml+server/users.tomlfrom the repo working tree, writes totmpdata/. - 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
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.
-
Run the script from the dev laptop:
./scripts/deploy-server.shPhases 1–2 create
/opt/stacks/doctate-server/{config,data,logs}and seed the three config files fromserver/*.example. Phase 3 then aborts with placeholder errors. -
Edit configs on minerva (via
nanoover SSH or through Dockge). Required fields:File Edit config/users.tomlAt least one [[user]]block with a realapi_keyand a real bcrypt hash forweb_passwordconfig/settings.toml[whisper] url,[ollama] urlfor the production hostsconfig/.envOptionally IONOS_API_KEY=…if any LLM backend uses IonosGenerate the bcrypt hash inside the container — the image is already loaded on minerva after phase 5 of run 1:
ssh minerva.lan 'docker run --rm -i doctate-server:latest hash-password'Copy the resulting
$2b$12$…line intousers.toml. -
Re-run the script:
./scripts/deploy-server.shThis 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:
./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:
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:
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:
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.