Docker

Operational guide only. For authoritative storage and metadata structure, see STORAGE.md. For behavior and invariants, see CONTRACT documents.

Docker Compose is the recommended way to run TranscriptX in containers (no local Python required). The Streamlit web interface runs inside the container with a mounted data directory.

This guide describes container behavior and operational layouts only. Canonical storage, output, and run-truth rules live in:

  • docs/runtime/STORAGE.md

  • docs/contracts/output-contract-v1.md

  • docs/run_outcome_contract.md

TranscriptX is analysis-only; it does not run WhisperX or any transcription engine inside Docker. Bring your own transcript JSON (see transcription.md for how to generate compatible transcripts).

Non-root /data write access

The default compose runs the transcriptx-web service as your host user (user: "${UID:-1000}:${GID:-1000}") so that files written under the mounted ./data volume are owned by you.

  • Dev / quick start: If /data is not writable (e.g. permission denied), make the host directory writable: chmod -R a+w data/ (or create data and then run compose).

  • Production: Use the same user: "${UID:-1000}:${GID:-1000}" so the container runs as a known UID/GID; ensure the host ./data is owned by that user or is group-writable.

Quickstart

Copy .env.example to .env and set HOST_RECORDINGS_DIR to an absolute path outside the repository. Then:

docker compose up transcriptx-web

Open http://localhost:8501. Compose builds the image on first run. First-time path: README and First analysis.

Build

docker build -t transcriptx:latest .

Set TRANSCRIPTX_TORCH_VARIANT at build time to control which PyTorch wheels are installed:

Value

When to use

default (unset)

Linux with NVIDIA GPU (nvidia-container-toolkit). Pulls CUDA wheels from PyPI on arm64.

cpu

Mac / Apple Silicon, or any CPU-only host. Skips 500MB+ NVIDIA deps; uses PyTorch CPU wheels.

With Compose (reads from .env or docker-compose.override.yml):

# Mac local dev (docker-compose.override.yml sets cpu by default)
docker compose build

# Explicit CPU build
TRANSCRIPTX_TORCH_VARIANT=cpu docker compose build

# GPU-oriented build (Linux)
TRANSCRIPTX_TORCH_VARIANT=default docker compose build

Plain docker build:

docker build --build-arg TRANSCRIPTX_TORCH_VARIANT=cpu -t transcriptx:latest .

Multi-arch (e.g. for publishing):

docker buildx build --platform linux/amd64,linux/arm64 -t transcriptx:latest .

The image includes the spaCy language models (en_core_web_sm, en_core_web_md, and en_core_web_lg), so NLP modules (topic modeling, NER, etc.) work out of the box—no need to run python -m spacy download inside the container.

Apple Silicon (M1/M2/M3): Prefer a CPU torch build — docker-compose.override.yml sets TRANSCRIPTX_TORCH_VARIANT=cpu for local dev. Docker on Mac cannot use GPU acceleration anyway. If arm64 builds still fail, you can build the amd64 image under emulation:

docker buildx build --platform linux/amd64 --load -t transcriptx:amd64 .
export HOST_RECORDINGS_DIR=/path/to/your/recordings   # outside the repo; same as in .env for compose
docker run --rm -v "$(pwd)/data:/data" -v "$HOST_RECORDINGS_DIR:/mnt/recordings" \
  -e TRANSCRIPTX_RECORDINGS_DIR=/mnt/recordings --platform linux/amd64 -p 8501:8501 transcriptx:amd64

Primary commands: Web interface

Start the web interface (port 8501):

docker compose up transcriptx-web

Then open http://localhost:8501 in your browser.

Host bind address (security)

Compose publishes the web port as:

ports:
  - "${TRANSCRIPTX_BIND_HOST:-127.0.0.1}:8501:8501"
  • Default: 127.0.0.1 (loopback only). Prefer this for local single-user use.

  • LAN opt-in: TRANSCRIPTX_BIND_HOST=0.0.0.0 docker compose up (recreate the single service; do not start a second stack).

  • Inside the container the app still uses --host 0.0.0.0 so the published host port can reach it.

Threat: binding to 0.0.0.0 grants unauthenticated LAN access to transcripts, generated artefacts, configuration-visible operations, and destructive cleanup actions in the UI. See SECURITY.md.

Canonical CI/release Compose proofs use only docker-compose.yml (or COMPOSE_FILE=docker-compose.yml) so a local docker-compose.override.yml cannot contaminate evidence.

You can also run docker compose up (without a service name) to start the web interface.

Scripting / automation (one-off Python API):

docker run --rm \
  -v "$(pwd)/data:/data" \
  -w /data \
  transcriptx:latest \
  python -c "
from transcriptx.app.models.requests import AnalysisRequest
from transcriptx.app.workflows.analysis import run_analysis
from pathlib import Path

result = run_analysis(AnalysisRequest(
    transcript_path=Path('/data/transcripts/foo_transcriptx.json'),
    modules=['stats'],
))
print('success:', result.success)
"

Operational modes

Mode

Command

Web interface

docker compose up or docker compose up transcriptx-web → http://localhost:8501

Unfamiliar-user / curated samples

docker compose -f docker-compose.unfamiliar-user.yml up transcriptx-web → http://127.0.0.1:8502 (isolated project; mounts ../transcriptx_test/ only — see below)

Custom host/port

docker run --rm -p 8501:8501 transcriptx:latest --host 0.0.0.0 --port 8501

Unfamiliar-user / disposable sample stack

docker-compose.unfamiliar-user.yml is a standalone Compose project (name: transcriptx-unfamiliar). It does not merge docker-compose.override.yml and does not touch the main ./data library.

Default host mounts (override with HOST_UNFAMILIAR_* env vars):

Host path

Container

../transcriptx_test/data

/data

../transcriptx_test/config

/data/.transcriptx

../transcriptx_test/transcriptx_test_transcripts

/mnt/transcripts

../transcriptx_test/transcriptx_test_recordings

/mnt/recordings

../transcriptx_test/transcriptx_test_outputs

/mnt/outputs

../transcriptx_test/transcript-inbox

/mnt/transcript-inbox

UI: http://127.0.0.1:8502 (TRANSCRIPTX_UNFAMILIAR_PORT to change). Use this for facilitator-supplied samples during unfamiliar-user validation.

After the first boot (or any wipe of ../transcriptx_test/data), ensure the epoch marker exists — Streamlit writes perf/ under /data, which makes an unmarked root fail the schema-epoch gate (missing_marker):

python -c "from pathlib import Path; from transcriptx.core.utils.schema_epoch import write_epoch; write_epoch(Path('../transcriptx_test/data'))"

Pre-copied managed transcripts under the transcripts mount are not auto-registered. Home counts the slug index (HOST_UNFAMILIAR_OUTPUT_DIR/.transcriptx_index.json). After the stack is up:

docker compose -f docker-compose.unfamiliar-user.yml exec -T transcriptx-web python - <<'PY'
from transcriptx.core.utils.file_discovery import discover_managed_transcript_paths
from transcriptx.io.admit_and_register import _try_register
for p in discover_managed_transcript_paths():
    print(_try_register(p), p.name)
PY

Then reload the UI (no recreate required if the volume is already mounted).

Volume layout

Compose mounts app working data at /data and maps host folders to container paths via HOST_* variables in .env (see .env.example):

volumes:
  - ./data:/data
  - ${HOST_CONFIG_DIR:-./data/.transcriptx}:/data/.transcriptx
  - transcriptx_cache:/home/transcriptx/.cache
  - ${HOST_TRANSCRIPTS_DIR:-./data/transcripts}:/mnt/transcripts:ro
  - ${HOST_TRANSCRIPT_INBOX_DIR:-./data/transcript-inbox}:/mnt/transcript-inbox:ro
  - ${HOST_OUTPUT_DIR:-./data/outputs}:/mnt/outputs
  - ${HOST_RECORDINGS_DIR}:/mnt/recordings
  - ${HOST_RECORDINGS_DIR}/imports:/mnt/recordings/imports
  - ${HOST_WAV_BACKUP_DIR:-./data/backups/wav}:/mnt/wav

HOST_RECORDINGS_DIR is required in .env and must point at a host folder outside the repository (your source-audio library). Create imports/ under that folder for uploads if needed.

Prefer HOST_CONFIG_DIR outside the clone as well (same idea as transcripts/outputs): it holds config.json (including saved custom questions), interface menus, and analysis profiles. Default remains ./data/.transcriptx.

Host variable

Container path

App env (TRANSCRIPTX_*)

Notes

(default) ./data

/data

TRANSCRIPTX_DATA_DIR=/data

App cache, groups, speaker_profiles, HF caches. Numba/librosa cache is NUMBA_CACHE_DIR=/tmp/numba_cache (not under /data) so Docker Desktop virtiofs does not break Numba’s cache-dir writability probe.

HOST_CONFIG_DIR (default ./data/.transcriptx)

/data/.transcriptx

TRANSCRIPTX_CONFIG_DIR=/data/.transcriptx

Project settings / metadata (config.json, menus, profiles). Prefer absolute path outside the clone

HOST_TRANSCRIPTS_DIR (default ./data/transcripts)

/mnt/transcripts

TRANSCRIPTX_TRANSCRIPTS_DIR=/mnt/transcripts

Read-only in base compose

HOST_TRANSCRIPT_INBOX_DIR (default ./data/transcript-inbox)

/mnt/transcript-inbox

(scan path only)

External inbox for Import all from folder; not under managed transcripts

HOST_OUTPUT_DIR (default ./data/outputs)

/mnt/outputs

TRANSCRIPTX_OUTPUT_DIR=/mnt/outputs

Analysis run outputs

HOST_RECORDINGS_DIR

/mnt/recordings

TRANSCRIPTX_RECORDINGS_DIR=/mnt/recordings

Source audio (read-only root)

HOST_RECORDINGS_DIR/imports

/mnt/recordings/imports

TRANSCRIPTX_IMPORTS_DIR=/mnt/recordings/imports

Writable uploads staging

HOST_WAV_BACKUP_DIR (default ./data/backups/wav)

/mnt/wav

TRANSCRIPTX_WAV_BACKUP_DIR=/mnt/wav

WAV archive

Local dev override: docker-compose.override.yml (optional, often gitignored) repeats these mounts but drops :ro on transcripts so the web UI can write speaker-map sidecars beside JSON files. It also mounts ./tests/fixtures at /mnt/fixtures:ro for deep-test analysis probes. Analyse those paths in place (TRANSCRIPTX_ALLOW_UNMANAGED_TRANSCRIPTS=1); do not import them into /mnt/transcripts. For production-like read-only transcripts, use only docker-compose.yml or remove the override.

Canonical storage layout and invariants: docs/runtime/STORAGE.md.

Environment variables

Variable

Default

Description

HOST_RECORDINGS_DIR

(required in .env)

Host path to source audio library (outside repo)

HOST_CONFIG_DIR

./data/.transcriptx

Host path mounted at /data/.transcriptx (project settings; prefer outside clone)

HOST_TRANSCRIPTS_DIR

./data/transcripts

Host path mounted at /mnt/transcripts

HOST_TRANSCRIPT_INBOX_DIR

./data/transcript-inbox

Host path mounted at /mnt/transcript-inbox (folder-import inbox)

HOST_OUTPUT_DIR

./data/outputs

Host path mounted at /mnt/outputs

HOST_WAV_BACKUP_DIR

./data/backups/wav

Host path mounted at /mnt/wav

STREAMLIT_SERVER_MAX_UPLOAD_SIZE

500 (in compose)

Max upload size in MB per file. Set in compose so the container allows 500 MB; without it Streamlit defaults to 200 MB.

TRANSCRIPTX_DATA_DIR

/data

Base data directory inside container

TRANSCRIPTX_CONFIG_DIR

/data/.transcriptx (compose)

Project config + menus + profiles (config.json holds saved custom questions)

TRANSCRIPTX_RECORDINGS_DIR

/mnt/recordings (compose)

Source audio

TRANSCRIPTX_IMPORTS_DIR

/mnt/recordings/imports (compose)

Writable upload staging

TRANSCRIPTX_TRANSCRIPTS_DIR

/mnt/transcripts (compose)

Transcript JSON files

TRANSCRIPTX_OUTPUT_DIR

/mnt/outputs (compose)

Analysis outputs

TRANSCRIPTX_SPEAKER_PROFILES_DIR

$TRANSCRIPTX_DATA_DIR/speaker_profiles

Longitudinal speaker profiles (PII) including enrolled voice under voice/; lives on the ./data bind mount so docker compose build / recreate keep it. Wiped only by Settings → Speakers revoke or per-profile Delete voice evidence. Optional override to keep names outside the clone

TRANSCRIPTX_WAV_BACKUP_DIR

/mnt/wav (compose)

WAV archive

TRANSCRIPTX_DISABLE_DOWNLOADS

0

Enable model/resource downloads (1 disables)

TRANSCRIPTX_HOST

0.0.0.0

Streamlit bind host

TRANSCRIPTX_PORT

8501

Streamlit port

Optional model overrides (TRANSCRIPTX_SPACY_MODEL, TRANSCRIPTX_SEMANTIC_MODEL, etc.) and LLM / Ollama settings (TRANSCRIPTX_LLM_ENABLED, TRANSCRIPTX_LLM_BASE_URL, TRANSCRIPTX_CORRECTIONS_LLM_ENABLED, …) are passed from the host .env into the container. See models.md, llm.md, and corrections-llm.md. On Mac/Windows Docker, point TRANSCRIPTX_LLM_BASE_URL at http://host.docker.internal:11434 so the container can reach Ollama on the host. Local docker-compose.override.yml defaults LLM + Corrections Studio discovery on when those vars are unset.

Health check

The compose file includes a health check that pings the Streamlit health endpoint:

docker compose ps   # shows health status

Pitfalls

  • Port conflict: If 8501 is taken, override with --port 8502 or set TRANSCRIPTX_PORT.

  • Permissions: Ensure the ./data directory is writable by the UID/GID used in compose.

  • Model downloads: Runtime downloads are enabled by default. Set TRANSCRIPTX_DISABLE_DOWNLOADS=1 for offline/no-download runs and provide pre-populated caches as needed.

  • Upload “AxiosError: Network Error”: If the file uploader shows this for large files, the server limit or a reverse proxy may be blocking the request. Compose sets STREAMLIT_SERVER_MAX_UPLOAD_SIZE=500; if you use a proxy in front, increase its body size and timeouts (e.g. nginx client_max_body_size and proxy_read_timeout).