pediatric-ai-scribe-v3/docker-compose.yml
Daniel 8d0dc968b3 feat: a deploy you can repeat, and prove afterwards
Reproducibility means two things here: the same commit builds the same image,
and the running container can be asked which commit it is.

  - Base images are pinned by digest, not by tag. A tag moves; two builds of one
    commit could otherwise differ. These are manifest-list digests, so buildx
    still picks the right architecture.

  - scripts/build-image.sh also writes ped-ai-local:<revision>, an immutable
    name a deploy can refer to instead of chasing :latest. Its summary goes to
    stderr so stdout stays the Compose invocation.

  - Compose takes the image from PED_AI_IMAGE, so a deploy runs a specific
    revision-tagged image while a local build still uses the local tag.

  - scripts/deploy.sh pins that image in the file Compose interpolates from,
    waits for health, then asks /api/build which revision is actually serving
    and rolls back to the previous image if it does not match. Healthy is not
    the same as running what you asked for. The rollback path was exercised.

  - The entrypoint applies migrations before the app starts, so code and schema
    arrive together. node-pg-migrate takes an advisory lock; losing it is not an
    error, it waits and looks again, so a rolling restart does not fail. A real
    migration failure stops the container rather than serving on a schema that
    does not match the build. RUN_MIGRATIONS=false opts out.

  - The Forgejo workflow builds through that same script, tags by full revision,
    and has an opt-in deploy job. It refuses to run if the deploy directory has
    uncommitted work rather than resetting over it.

The running image was labelled revision=unknown, and /api/build said "unknown",
because `docker compose up --build` never passes GIT_REVISION. That is exactly
the hole this closes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 00:41:11 +02:00

124 lines
4.5 KiB
YAML

services:
pediatric-scribe:
build:
context: .
args:
GIT_REVISION: ${GIT_REVISION:-unknown}
# A deploy sets PED_AI_IMAGE to an immutable, revision-tagged image from the
# registry; a local build leaves it unset and uses the tag build-image.sh
# writes. Either way the running container can be asked what it is:
# /api/build returns the revision baked into it at build time.
image: ${PED_AI_IMAGE:-ped-ai-local:latest}
ports:
- "127.0.0.1:3552:3000"
env_file:
- .env
environment:
CLINICAL_ASSISTANT_MCP_URL: http://mcp:8000/mcp
REDIS_URL: redis://ped-ai-redis:6379
LOKI_URL: http://monitoring-loki:3100
LITELLM_API_BASE: http://litellm:4000
TTS_PROVIDER: litellm
LITELLM_TTS_MODEL: local-kokoro-tts
LITELLM_TTS_VOICE: sherpa/kokoro:am_adam
LITELLM_TTS_VOICES: sherpa/kokoro:am_adam,sherpa/kokoro:am_michael,sherpa/kokoro:af_bella,sherpa/kokoro:af_nicole,sherpa/kokoro:bf_emma,sherpa/kokoro:bm_lewis
CLINICAL_ASSISTANT_PROMPT_POOL_TARGET: 1000
LIBRETRANSLATE_URL: ${LIBRETRANSLATE_URL:-http://libretranslate:5000}
DEEPL_API_BASE: ${DEEPL_API_BASE:-https://api.deepl.com/v2}
GENERATED_IMAGES_S3_ENDPOINT: http://assets:9000
GENERATED_IMAGES_S3_REGION: us-east-1
GENERATED_IMAGES_S3_BUCKET: generated-images
GENERATED_IMAGES_S3_ACCESS_KEY_FILE: /run/secrets/generated-images-access-key
GENERATED_IMAGES_S3_SECRET_KEY_FILE: /run/secrets/generated-images-secret-key
# Recordings are kept for 24 hours; the same MinIO, its own bucket. The
# app key carries a second policy covering only audio-backups, so these
# can reuse the mounted credentials (see scripts/enable-audio-backup-bucket.js).
AUDIO_BACKUPS_S3_ENDPOINT: http://assets:9000
AUDIO_BACKUPS_S3_REGION: us-east-1
AUDIO_BACKUPS_S3_BUCKET: audio-backups
AUDIO_BACKUPS_S3_ACCESS_KEY_FILE: /run/secrets/generated-images-access-key
AUDIO_BACKUPS_S3_SECRET_KEY_FILE: /run/secrets/generated-images-secret-key
volumes:
- scribe-logs:/app/data/logs
- clinical-assistant-mcp-data:/app/mcp-data:ro
- /home/danvics/docker/personal-assistant-storage-milvus/secrets/images-access-key:/run/secrets/generated-images-access-key:ro
- /home/danvics/docker/personal-assistant-storage-milvus/secrets/images-secret-key:/run/secrets/generated-images-secret-key:ro
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
container_name: pediatric-ai-scribe
networks:
- default
- danvics_mcp
- danvics_monitoring
- danvics_speech
- ped-ai-storage-assets
- openwebui_openwebui
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
postgres:
# Digest-pinned, so a rebuilt environment gets this exact Postgres. If a
# newer pg16 image ships a different ICU library, the startup drift check in
# src/db/database.js auto-REINDEXes and refreshes the collation version;
# pinning means that only happens when this line is deliberately changed.
image: pgvector/pgvector:pg16@sha256:00ba258a66dac104fd5171074a0084462a64a1369d8513f3d0a634e2f24d15bc
environment:
POSTGRES_DB: pedscribe
POSTGRES_USER: pedscribe
POSTGRES_PASSWORD: ${DB_PASSWORD:-pedscribe}
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
container_name: pedscribe-db
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pedscribe"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:8-alpine@sha256:d146f83b1e0f02fc27c26a50cee39338c736674c5959db84363e6ae3cd9e02d2
command: redis-server --appendonly yes
restart: unless-stopped
container_name: ped-ai-redis
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- default
- danvics_mcp
volumes:
pgdata:
scribe-logs:
redis-data:
clinical-assistant-mcp-data:
external: true
name: mcp-server_mcp-data
networks:
danvics_mcp:
external: true
danvics_monitoring:
external: true
danvics_speech:
external: true
ped-ai-storage-assets:
external: true
name: personal-assistant-storage-milvus_assets
openwebui_openwebui:
external: true
name: open-webui_openwebui-network