Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m38s
Forgejo Docker Build / Build Docker image (push) Successful in 12s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s
Three pairs of docs described the same thing twice, and the copies had drifted
apart. Merged each into one file, keeping the unique content from both:
- ARCHITECTURE.md -> architecture.md (its operational map: ownership, request
flow, runtime boundaries, source of truth, deployment shape)
- DEVELOPMENT.md -> developer-guide.md (change workflow, Clinical Assistant
high-risk areas, frontend rendering rules, deployment checks)
- transcription-options.md -> speech.md (the clinic setup table, and the list
of browser-Whisper paths that must stay removed)
Then audited what remained against the code and the live database rather than
against the previous docs. Corrected:
- Google Vertex was still documented as a provider across nine files. The SDK
is gone; AI_PROVIDER=vertex now logs an advisory and falls back to
OpenRouter, and Gemini is reached through LiteLLM. Fixed the provider
selection order to match src/utils/ai.js, which starts from LITELLM_API_BASE.
- promptSafe was documented on 8 routes; it is on 13.
- Node 20 -> 24, "24 vanilla JS modules" -> no fixed count, and
transcribe.js/tts.js -> sttProvider.js/ttsProvider.js, which is what exists.
- STT/TTS are LiteLLM-only; README listed direct Google, AWS Transcribe and
ElevenLabs paths that are not in the runtime.
- Learning Hub PPTX export was documented as pptxgenjs, which is not a
dependency. It is pandoc against a reference deck.
- POST /api/admin/milestones/seed does not exist; it is /bulk-import.
- NEXTCLOUD_URL and NTFY_TOPIC are not read anywhere. Nextcloud is per-user in
the users table, and the ntfy topic is derived as pedscribe-{userId}.
- A prose paragraph sat inside the Clinical Assistant settings table, so half
the rows rendered as text.
Filled the gaps the audit exposed:
- database.md was missing 12 of 29 tables, including user_resources,
personal_notes, login_codes, registration_invites and generated_image_jobs.
- developer-guide.md was missing 11 routers and 10 frontend modules.
- api-reference.md detailed 121 of 244 endpoints and said so, but whole
features were absent. Added an endpoint index covering Clinical Assistant,
My Resources, Notes, Diagrams, ED Encounters, invites and sign-in codes.
- configuration.md was missing METRICS_TOKEN, REDIS_URL, API_RATE_LIMIT_MAX,
the LITELLM_* model variables, the DB_* ones maintenance.js reads, and the
per-purpose S3 resolution scheme.
- clinical-assistant.md documented 2 of its 17 environment variables.
- features-explained.md had no entry for My Resources or Clinical Assistant.
Renamed the three remaining SHOUTING filenames to kebab-case, which is what the
docs viewer's prettyName() was working around, and rewrote README's index,
which listed architecture.md twice and omitted nine files.
Noted but not changed: the Turnstile site key is hardcoded in index.html rather
than read from TURNSTILE_SITE_KEY, and /api/health/detailed can report
tts: 'elevenlabs' though no ElevenLabs path exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
246 lines
9.2 KiB
Markdown
246 lines
9.2 KiB
Markdown
# Deployment
|
|
|
|
## Prerequisites
|
|
|
|
- Docker + Docker Compose
|
|
- Reverse proxy (Caddy, Nginx, Traefik) for TLS termination
|
|
- At least one configured AI provider (LiteLLM / OpenRouter / Bedrock / Azure)
|
|
|
|
## What the image carries
|
|
|
|
Beyond Node, the runtime image installs a few tools that document export depends
|
|
on. They are in `Dockerfile` and worth knowing about before trimming it:
|
|
|
|
| | For |
|
|
|---|---|
|
|
| `pandoc-cli` | the fallback for Word export when the renderer cannot run |
|
|
| `python3`, `py3-lxml`, `py3-pillow` | the slide renderer. Both libraries are C extensions with no Alpine wheels, so they come from apk rather than pip — installing them from source would mean carrying a compiler in the runtime image |
|
|
| `python-pptx==1.0.2`, `python-docx==1.1.2` (pip) | build the decks and the documents. Pinned: unpinned, a rebuild from the same commit could produce different output |
|
|
| `poppler-utils` | `pdftoppm`, which turns a rendered deck into one image per slide so a vision model can see it. Only needed when slide review is switched on |
|
|
| `ffmpeg`, `curl`, `jq` | audio handling and entrypoint scripting |
|
|
|
|
Roughly 58MB of that is Python. PDF conversion is **not** in the image — it goes
|
|
to Gotenberg over the network (`GOTENBERG_URL`, default `http://gotenberg:3000`),
|
|
so PowerPoint and Word still work when Gotenberg is down and only PDF fails.
|
|
|
|
See [my-resources.md](my-resources.md) for what the renderer does.
|
|
|
|
## Images
|
|
|
|
| Image | Role |
|
|
|---|---|
|
|
| `danielonyejesi/pediatric-ai-scribe-v3:latest` | App container. Published by CI on every tag push where configured. Pull directly or build from source. |
|
|
| `pgvector/pgvector:pg16` | Database. |
|
|
| `redis:7-alpine` | Operational Redis cache/state. |
|
|
|
|
## 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
|
|
./scripts/build-image.sh
|
|
docker compose up -d --no-build
|
|
```
|
|
|
|
The build uses Node 24 LTS and `npm ci --omit=dev` from the root lockfile.
|
|
`./scripts/build-image.sh` resolves the full checkout Git commit (including
|
|
worktrees/packed refs) and passes `GIT_REVISION` through Compose. It only builds;
|
|
starting or replacing production services remains a separate reviewed step.
|
|
Use `COMPOSE_FILE=docker-compose.local.yml ./scripts/build-image.sh` for the local
|
|
variant. For direct Docker builds:
|
|
|
|
```bash
|
|
docker build --build-arg GIT_REVISION="$(git rev-parse --verify 'HEAD^{commit}')" -t ped-ai-local:latest .
|
|
```
|
|
|
|
All Compose variants accept the same `GIT_REVISION` environment variable. A build without one is explicitly
|
|
`unknown` (unversioned development), not a release provenance claim.
|
|
|
|
The Dockerfile rejects malformed revisions and writes the same full SHA to
|
|
`/app/BUILD_ID` and `org.opencontainers.image.revision`. `/api/build`, the
|
|
`X-Build-Id` header and asset query strings use that baked value. Git identifies
|
|
the source commit, not local uncommitted changes: release from a clean checkout;
|
|
a local dirty test image is not an exact representation of that commit.
|
|
|
|
Forgejo's existing trusted push/manual release workflows run a Node 24 root
|
|
`npm ci` / `npm test` job on `forgejo-local`; APK and Docker jobs require it via
|
|
`needs`. No untrusted pull-request code may run on that privileged runner.
|
|
An isolated, unprivileged Forgejo PR runner is separate future provisioning,
|
|
not an assumed label in these workflows. GitHub-hosted PR CI uses Node 24;
|
|
GitHub release workflows also gate builds on root tests. Mobile dependency
|
|
versions and signing/publishing gates are unchanged.
|
|
|
|
The default compose starts `pediatric-ai-scribe` on `127.0.0.1:3552`, `pedscribe-db` internally, and `ped-ai-redis` internally.
|
|
|
|
## 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) | High for compliance evidence; Postgres also has audit/API/access tables |
|
|
|
|
### 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
|
|
./scripts/build-image.sh --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 |
|
|
| `GET /metrics` | Prometheus metrics in text exposition format |
|
|
|
|
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: Postgres size scales with audit log retention, saved encounters, documents, and Learning Hub content.
|
|
- 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 or Azure OpenAI directly, or a LiteLLM gateway pointed at a BAA-eligible upstream. Not OpenRouter.
|
|
- SMTP configured for verification + reset emails
|
|
- Turnstile keys set for public-facing deployments
|
|
- Reverse proxy serves valid TLS certs
|
|
- Postgres dump scheduled off-host
|
|
- Log retention and backup policy covers `audit_log`, `api_log`, `access_log`, and filesystem `scribe-logs`
|
|
|
|
## CI / CD
|
|
|
|
On push (and tag push), these workflows run (depending on runner/site):
|
|
|
|
| Workflow | Output | Runtime |
|
|
|---|---|---|
|
|
| `.forgejo/workflows/android-apk.yml` | Signed APK attached to the Forgejo release, plus optional Google Play internal track upload | ~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 |
|
|
| Redis | 6379 | 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.
|
|
|
|
A central Prometheus/Loki/Grafana stack can also scrape `GET /metrics` and collect Docker logs with Promtail. Keep direct Loki push enabled only for structured application events that are useful for compliance and operations.
|
|
|
|
## 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`.
|