Nothing documented My Resources, the slide renderer, PubMed or web search, and the authentication doc predated both sign-in codes and registration invitations. docs/my-resources.md is new and covers the feature end to end: what a resource is, where its material comes from, why both searches run in the route rather than as tools the model never called, why keyword engines get the topic while retrieval gets the instruction too, how a presentation is designed as a deck rather than written as markdown, the separate multi-image path, and what the export pipeline is made of. docs/authentication.md gains sign-in codes — storage, lifetime, reuse, supersession, guessing, and that two-factor still applies — and registration invitations, including the exact condition that decides when a code may be deleted and why it is written to match the status the list displays. Both new rate limits are in the table, with a note that Express matches app.use paths on segment boundaries, so a new sign-in endpoint needs its own limiter or it has none at all. docs/deployment.md now says what the runtime image carries and why — pandoc for Word, python3 with apk-installed lxml and pillow for the slide renderer, python-pptx pinned, and that PDF conversion is not in the image at all but goes to Gotenberg, so Word and PowerPoint still work when it is down. docs/configuration.md picks up LOGIN_RATE_LIMIT_MAX, LOGIN_CODE_RATE_LIMIT_MAX and GOTENBERG_URL, none of which were listed. README gains a My Resources section and indexes the two new docs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
8.9 KiB
Deployment
Prerequisites
- Docker + Docker Compose
- Reverse proxy (Caddy, Nginx, Traefik) for TLS termination
- At least one configured AI provider (Bedrock / Azure / Vertex / LiteLLM / OpenRouter)
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 |
Word (.docx) export |
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 (pip) |
builds the decks. Pinned: unpinned, a rebuild from the same commit could produce different slides |
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 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
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:
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
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
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
# 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
docker compose pull
docker compose up -d
Building from source
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_KEYexactly 64 hex charsDB_PASSWORDnon-defaultAPP_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
- Log retention and backup policy covers
audit_log,api_log,access_log, and filesystemscribe-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
- Container stdout (
docker compose logs -f pediatric-scribe). - Filesystem
data/logs/YYYY-MM-DD.log(JSONL, one line per event). - Postgres tables
audit_log,api_log,access_log— batched writes viasrc/utils/auditQueue.js, drained on SIGTERM. - Loki (if
LOKI_URLset) — 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:
- Close HTTP listener (new connections refused, in-flight finish).
- Drain
src/utils/auditQueue.js(flush any pending audit/api/access writes). 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.