Skip to main content
Tale runs as six Docker containers managed by Docker Compose. Each container has a single responsibility and communicates over an internal bridge network.
Phase 2 split architecture (since v0.2.34): Convex runs as its own service (convex) instead of being embedded in the platform container. Platform becomes a thin Vite client that pushes schema + env to Convex over HTTP. See Upgrading from pre-split-convex (pre-v0.2.34) for the one-time migration step.

Service overview

Image details

ServiceBase imageOptimized sizeBuild strategy
Platformghcr.io/get-convex/convex-backend (for generate_key glibc binary)~320 MB compressed5-stage: deps → builder → pruner → runner → squash
Convexghcr.io/get-convex/convex-backend~485 MB compressed2-stage: dashboard → runner (Dashboard COPY-ed from upstream image)
Crawlerpython:3.11-slim~650 MB compressed3-stage: builder → runtime → squash. Chromium headless_shell only
RAGpython:3.11-slim~515 MB3-stage: builder → runtime → squash. libpq5 only
DBparadedb/paradedb:0.22.5-pg16~1.06 GB3-stage: cleanup → runtime → squash
Proxycaddy:2.11-alpine~88 MBSingle stage
Splitting Convex out of platform reduced the platform image from ~2.58 GB to ~320 MB compressed; the convex service is a new ~485 MB image. Net image disk is similar but the platform layer rebuilds much faster for app-only changes.

Port mapping

Development ports (compose.yml)

ServiceHost portContainer portProtocol
DB54325432TCP (PostgreSQL)
Crawler80028002HTTP
RAG80018001HTTP
Convex3210, 3211, 6791WS/HTTP (via proxy)
Platform3000HTTP (via proxy)
Proxy80, 44380, 443HTTP/HTTPS

Test ports (compose.test.yml)

ServiceHost portContainer port
DB154325432
Crawler180028002
RAG180018001
Convex13210, 13211, 167913210, 3211, 6791
Platform130003000
Proxy10080, 1044380, 443

Volume mapping

VolumeMounted inPathPurpose
db-dataDB/var/lib/postgresql/dataPostgreSQL data directory
db-backupDB/var/lib/postgresql/backupDatabase backups
rag-dataRAG/app/dataTemp files, document processing
crawler-dataCrawler/app/dataWebsite registry, URL databases
convex-dataConvex/app/dataConvex DB (SQLite/pg-local), search indexes, files, agents/workflows/integrations/providers JSON
convex-dataPlatform/app/data (read-only)Config SSE watcher + branding image serving
convex-dataCrawler, RAG/app/platform-config (read-only)Shared provider config
caddy-dataProxy, Convex/data, /caddy-dataTLS certificates
caddy-configProxy/configCaddy configuration
platform-data(legacy, unmounted)Preserved after upgrade for rollback safety; remove manually after verifying the split: docker volume rm <projectId>_platform-data
Important: Never run docker compose down -v. The -v flag deletes all Docker volumes, permanently erasing your database and all platform data.

Build arguments

ArgumentDefaultUsed byDescription
VERSIONdevAllImage version tag (set by CI from git tag)
INSTALL_CJK_FONTSfalseCrawlerInstall CJK font support (~100 MB)

Multi-stage build strategy

All services use a FROM scratch squash as their final stage. This flattens Docker layers so that file deletions in cleanup stages actually reclaim disk space, rather than just adding masking layers.

Platform (5 stages, post-split)

  1. bun-bin — Extracts Bun binary
  2. workspace-deps — Installs all npm dependencies (including devDependencies)
  3. builder — Runs vite build to produce the SPA
  4. pruner — Reinstalls production-only deps, removes dev packages (@vitest, @storybook, typescript, etc.)
  5. runner — Final runtime on the convex-backend base image (kept for the generate_key glibc binary used to sign Convex admin tokens). Vite SPA + Bun server only — no Convex backend process.
  6. squashFROM scratch + COPY --from=runner. Runs as root, drops to app user via gosu in entrypoint.

Convex (2 stages, new in Phase 2)

  1. convex-dashboardFROM ghcr.io/get-convex/convex-dashboard to COPY the Next.js standalone build.
  2. runnerFROM ghcr.io/get-convex/convex-backend. Contains convex-local-backend daemon, Dashboard, builtin seed assets (agents/workflows/integrations/providers/branding), entrypoint. Strips LLVM/Clang (~155 MB).

Crawler (3 stages)

  1. builder — Installs Python deps via uv, downloads Chromium headless_shell, runs deep cleanup (removes full Chrome, FFmpeg, pip, __pycache__, .so debug symbols, test dirs)
  2. runtime — Clean python:3.11-slim with only runtime system libs (Chromium deps, tini, curl). Strips LLVM/Adwaita icons
  3. squashFROM scratch + COPY --from=runtime. Pre-creates volume mountpoints for /app/data and /app/platform-config

RAG (3 stages)

  1. builder — Installs Python deps with build-essential + libpq-dev for compiling native packages, then strips pip/setuptools
  2. runtime — Clean python:3.11-slim with only libpq5 + curl. Pre-creates volume mountpoints
  3. squashFROM scratch + COPY --from=runtime

DB (3 stages)

  1. cleanup — Strips debug symbols (~888 MB), LLVM shared libraries (~127 MB), PostGIS extension files, locales, and docs from the ParadeDB base image
  2. runtimeFROM scratch + COPY --from=cleanup. Fresh layer with only cleaned files
  3. squash — Re-declares PGDATA, PATH, and other ENV vars lost during FROM scratch

Health checks

ServiceEndpointProtocolStart period
DBpg_isready -U tale -d taleCLI60s
CrawlerGET /health on :8002HTTP40s
RAGGET /health on :8001HTTP40s
ConvexGET :3210/version + [ -f /tmp/convex-ready ]HTTP + file60s
PlatformGET :3000/api/health + [ -f /tmp/platform-ready ]HTTP + file180s
ProxyGET /health on :2020 (internal)HTTP10s
The /tmp/<service>-ready markers are touched by each service’s entrypoint after its one-shot initialization work completes (Convex: backend up + builtin seed; Platform: env sync + convex deploy). This prevents traffic from being routed before the service is truly ready to serve requests.

Container testing

Tale includes three container test scripts:
ScriptCommandWhat it tests
tests/container-smoke-test.shbun run docker:testBuilds, starts, health checks, HTTP endpoints, inter-service connectivity
tests/container-image-test.shbun run docker:test:imageOCI labels, non-root user, no secrets, HEALTHCHECK instruction, size budgets
tests/container-vulnerability-scan.shbun run docker:test:vulnerabilityTrivy vulnerability scan (HIGH + CRITICAL)
See Contributing Docker guide for details on modifying Dockerfiles and running tests.
Last modified on April 19, 2026