- Drop first/second-person voice; reference-style prose throughout - Remove stale information; align with current code (argon2id primary, hybrid cookie/Bearer auth, sliding 24h idle, AES-256-GCM PHI at rest, backup codes, node-pg-migrate, collation-drift guard, multi-arch Docker, auto-version pipeline) - Preserve all technical accuracy and code examples - Remove any remaining references to separate PedsHub Quiz app - Keep consistent tone across files (tables + code blocks, imperatives where needed) - api-reference.md and developer-guide.md route tables expanded to reflect current routes (billing, sessions)
193 lines
5.7 KiB
Markdown
193 lines
5.7 KiB
Markdown
# Deployment
|
|
|
|
## Prerequisites
|
|
|
|
- Docker + Docker Compose
|
|
- Reverse proxy (Caddy, Nginx, Traefik) for TLS termination
|
|
- At least one configured AI provider (Bedrock / Azure / Vertex / LiteLLM / OpenRouter)
|
|
|
|
## Images
|
|
|
|
| Image | Role |
|
|
|---|---|
|
|
| `danielonyejesi/pediatric-ai-scribe-v3:latest` | App container. Published by CI on every tag push (multi-arch: `linux/amd64` + `linux/arm64`). Pull directly or build from source. |
|
|
| `pgvector/pgvector:pg16` | Database. |
|
|
|
|
## Build from source
|
|
|
|
```bash
|
|
git clone https://github.com/ifedan-ed/pediatric-ai-scribe-v3.git
|
|
cd pediatric-ai-scribe-v3
|
|
cp .env.example .env
|
|
# edit .env — required: APP_URL, JWT_SECRET, DATA_ENCRYPTION_KEY, DB_PASSWORD, an AI provider
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Two containers come up: `pediatric-ai-scribe` on `127.0.0.1:3552`, `pedscribe-db`
|
|
internal only.
|
|
|
|
## Minimum `.env`
|
|
|
|
```env
|
|
APP_URL=https://scribe.example.com
|
|
JWT_SECRET=<openssl rand -hex 32>
|
|
DATA_ENCRYPTION_KEY=<openssl rand -hex 32>
|
|
DB_PASSWORD=<strong password>
|
|
|
|
AI_PROVIDER=litellm
|
|
LITELLM_API_BASE=https://llm.example.com
|
|
LITELLM_API_KEY=sk-...
|
|
```
|
|
|
|
Full variable reference: `docs/configuration.md`.
|
|
|
|
## Reverse proxy
|
|
|
|
App binds to `127.0.0.1:3552` only. TLS termination + host routing is the
|
|
proxy's job.
|
|
|
|
### Caddy
|
|
|
|
```
|
|
scribe.example.com {
|
|
reverse_proxy localhost:3552
|
|
}
|
|
```
|
|
|
|
### Nginx
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name scribe.example.com;
|
|
ssl_certificate /etc/ssl/certs/scribe.example.com.pem;
|
|
ssl_certificate_key /etc/ssl/private/scribe.example.com.key;
|
|
client_max_body_size 100M;
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3552;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}
|
|
```
|
|
|
|
App sets `trust proxy: 1` so rate limiting uses the original client IP.
|
|
|
|
## Volumes
|
|
|
|
| Volume | Contents | Backup priority |
|
|
|---|---|---|
|
|
| `pgdata` | All user data, encounters, memories, audit logs, settings, embeddings | Critical |
|
|
| `scribe-logs` | Filesystem audit log files (JSONL by day) | Low — Postgres also has these in `audit_log` table |
|
|
|
|
### Postgres backup / restore
|
|
|
|
```bash
|
|
# Backup
|
|
docker exec pedscribe-db pg_dump -U pedscribe pedscribe > backup.sql
|
|
|
|
# Restore
|
|
cat backup.sql | docker exec -i pedscribe-db psql -U pedscribe pedscribe
|
|
```
|
|
|
|
## Updating
|
|
|
|
### From a Docker Hub pull
|
|
|
|
```bash
|
|
docker compose pull
|
|
docker compose up -d
|
|
```
|
|
|
|
### Building from source
|
|
|
|
```bash
|
|
git pull
|
|
docker compose build --no-cache
|
|
docker compose up -d
|
|
```
|
|
|
|
On startup the container runs `initDatabase()` (idempotent baseline), then
|
|
`node-pg-migrate` applies any new migration files. Collation-drift check auto-
|
|
REINDEXes if the ICU library version changed between image builds.
|
|
|
|
## Health
|
|
|
|
| Endpoint | Purpose |
|
|
|---|---|
|
|
| `GET /api/health` | `{ok:true}` — public, used by Docker health check |
|
|
| `GET /api/health/detailed` | Provider status — admin-auth required |
|
|
| `GET /api/build` | Build ID (short git SHA) — useful for debugging cache invalidation |
|
|
|
|
Docker health check in `Dockerfile`: every 30 s, wget-spiders `/api/health`.
|
|
Container marked unhealthy after 5 failures.
|
|
|
|
## Resource footprint
|
|
|
|
- RAM: 256 MB minimum, 512 MB recommended for one instance with a handful of concurrent users.
|
|
- Disk: ~220 MB image (self-hosted Whisper WASM included). Postgres size scales with audit log retention.
|
|
- CPU: idle load negligible; AI calls are network-bound on the LLM provider side.
|
|
|
|
## Production checklist
|
|
|
|
- `JWT_SECRET` ≥ 32 bytes (`openssl rand -hex 32`)
|
|
- `DATA_ENCRYPTION_KEY` exactly 64 hex chars
|
|
- `DB_PASSWORD` non-default
|
|
- `APP_URL` = public URL (enables fail-closed CORS + HSTS + secure cookies)
|
|
- HIPAA workload → use Bedrock, Azure OpenAI, or Vertex (all BAA-eligible). Not OpenRouter or ElevenLabs.
|
|
- SMTP configured for verification + reset emails
|
|
- Turnstile keys set for public-facing deployments
|
|
- Reverse proxy serves valid TLS certs
|
|
- Postgres dump scheduled off-host
|
|
|
|
## CI / CD
|
|
|
|
Four workflows fire on tag push:
|
|
|
|
| Workflow | Output | Runtime |
|
|
|---|---|---|
|
|
| `android-release.yml` | Signed APK attached to the GitHub release | ~8 min |
|
|
| `docker-publish.yml` | Multi-arch image (amd64 + arm64 via native runners) on Docker Hub | ~4 min |
|
|
| `build-apk.yml` | Legacy TWA APK (optional second artifact) | ~2 min |
|
|
|
|
Triggered by `auto-version.yml` (reads commit messages, bumps + tags via
|
|
`RELEASE_PAT`) or manually via `Actions → Version bump & release` or
|
|
`scripts/release.sh X.Y.Z --push`.
|
|
|
|
## Ports
|
|
|
|
| Service | Internal | External default |
|
|
|---|---|---|
|
|
| App | 3000 | 127.0.0.1:3552 |
|
|
| Postgres | 5432 | not exposed |
|
|
|
|
Change the app's external port by editing the `ports:` mapping in
|
|
`docker-compose.yml`.
|
|
|
|
## Log destinations
|
|
|
|
1. Container stdout (`docker compose logs -f pediatric-scribe`).
|
|
2. Filesystem `data/logs/YYYY-MM-DD.log` (JSONL, one line per event).
|
|
3. Postgres tables `audit_log`, `api_log`, `access_log` — batched writes
|
|
via `src/utils/auditQueue.js`, drained on SIGTERM.
|
|
4. Loki (if `LOKI_URL` set) — pushed fire-and-forget per event.
|
|
|
|
## Auto-cleanup
|
|
|
|
| Target | Policy | Frequency |
|
|
|---|---|---|
|
|
| `saved_encounters` | Delete where `expires_at < NOW()`. Default 7 days (configurable via `site.auto_delete_days`). | Hourly + 10 s after startup |
|
|
| `audio_backups` | Delete where `expires_at < NOW()` (24 h default). | Same schedule |
|
|
|
|
## Graceful shutdown
|
|
|
|
`server.js` handles `SIGTERM` and `SIGINT`:
|
|
|
|
1. Close HTTP listener (new connections refused, in-flight finish).
|
|
2. Drain `src/utils/auditQueue.js` (flush any pending audit/api/access writes).
|
|
3. `pool.end()` — close Postgres pool cleanly.
|
|
|
|
9-second hard deadline — Docker sends `SIGKILL` after 10 s by default. Prevents
|
|
in-flight note writes from being truncated on `docker restart`.
|