Files
Brummel 03129ad592 chore: add restart-server.sh helper for fast minerva restarts
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.
2026-05-04 11:35:08 +02:00

5.7 KiB
Raw Permalink Blame History

Deployment

The doctate-server runs in two flavors that can coexist:

  • Local devcargo 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

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:

    ./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:

    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:

    ./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:

./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:

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.