03129ad592
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.
158 lines
5.7 KiB
Markdown
158 lines
5.7 KiB
Markdown
# 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.
|