Contributing to Docker images
How to build and extend Tale's Docker images for forks, vendored builds, or air-gapped distributions.
4 min read
Every container Tale ships has its Dockerfile in the public source repo. Forks, air-gapped distributions, and one-off patches all start from the same files; this page is the operator's walk through building the images yourself, where the customisation seams live, and how to keep a fork in sync with upstream without diverging on the boring parts.
The container architecture lives at Container architecture; this page is what you read when the published images do not fit and you need to build your own.
What the images are
The stack is entirely TypeScript — no Python image. Each image has one Dockerfile under services/<name>/:
| Image | Source path | Base |
|---|---|---|
tale-proxy | services/proxy/ | Caddy |
tale-platform | services/platform/ | Bun + Debian slim |
tale-convex | services/convex/ | Convex local-backend |
tale-db | services/db/ | ParadeDB (Postgres) |
tale-sandbox | services/sandbox/ | Bun + Docker CLI |
tale-sandbox-egress | services/sandbox-egress/ | Alpine + tinyproxy |
tale-sandbox-runtime | services/sandbox-runtime/ | Bun + Chromium + Playwright |
tale-sandbox-buildkitd | services/sandbox-buildkitd/ | Debian + BuildKit + redsocks |
tale-controller | services/controller/ | Bun + Docker CLI |
Both database containers — db and knowledge-db — build from the same tale-db ParadeDB image; the difference is the database each one serves. The LLM gateway, tale-sandbox-llm-gateway, is a pinned upstream image (maximhq/bifrost), so it has no Dockerfile in the repo. The compose files at the repo root (compose.yml for development, the CLI-generated production compose) reference these by ghcr.io/tale-project/tale/<image>:<tag>. A local build replaces the registry pull with a build: block in compose.
Building locally
A first build of every image takes about 15 minutes on a recent laptop; subsequent builds hit Docker's layer cache and finish in under a minute for the image you changed.
# Build every image in compose.yml
docker compose build
# Build one image
docker compose build platformSet PULL_POLICY=build in your environment (or in .env) to force compose to build rather than pull the published image. The shipped compose.yml defaults to build, so a local clone with no overrides already builds; production compose files generated by tale deploy default to always and pull from the registry.
The customisation seams
The supported extension points for forks are at the Dockerfile level. The image's entrypoint and the configuration files inside it are stable — patch them, build the image, and the rest of the system does not need to know.
- Caddyfile —
services/proxy/Caddyfilecontrols routing and TLS termination. Custom headers, custom subdomains, and custom rate limits land here. - Platform plop templates —
services/platform/Dockerfileruns a build step that bakes in the messages, the schema, and the static assets. A fork that ships custom UI strings or extra routes builds the platform image. - Sandbox runtime image —
services/sandbox-runtime/Dockerfileis the execution environment forRun code, web rendering, and document generation; it already carries Chromium and Playwright. A fork that needs an extra system package or a different browser build patches here. - Sandbox egress proxy —
services/sandbox-egress/tinyproxy.conf.templateis the proxy config the entrypoint renders at startup: open egress by default, or a default-deny hostname filter whenSANDBOX_EGRESS_ALLOWLISTis set. A fork that needs different proxy behaviour patches here.
What is not a supported seam: the convex backend's application code, including document extraction and the RAG and crawler logic that now live in-process (services/platform/convex/), and the platform container's runtime code (services/platform/app/). Those files are application code, not configuration — adding a document-format extractor or changing retrieval behaviour is a real fork and rides the upgrade tax.
Tagging and pushing your own registry
For air-gapped or vendored distributions, the path is "build, tag, push to your registry, change the compose image: lines."
# Build, tag, push
export REGISTRY=registry.internal.example.com/tale
docker compose build
docker tag ghcr.io/tale-project/tale/tale-platform:latest \
$REGISTRY/tale-platform:vendored-1.0
docker push $REGISTRY/tale-platform:vendored-1.0The CLI's deploy generates a compose file with the registry path; either patch the generated file post-generation, or skip the CLI and run docker compose directly against a compose file you maintain yourself.
Staying in sync with upstream
The cheap path is a fork on GitHub that periodically merges from tale-project/tale@main. Conflicts land in the files you patched; the rest carries through clean. The two anti-patterns:
- Patching application code instead of contributing it back. If the change is broadly useful, upstream a PR — every release tax goes down.
- Pinning to an old base image. The Caddy, Bun, and Postgres bases pick up security patches on rebuild; pinning the base for "stability" is borrowing trouble.
Where this fits
This page is the contributor-facing seam of the operator story. The architecture overview lives in Container architecture; the upgrade workflow that runs the published images is in Upgrades. If your fork is non-trivial, the conversation worth starting before you write code is the one on the project's Discord or GitHub Discussions — many forks end up being features waiting to land upstream.