Self-hosting means you run the same three layers a timbal create project contains — UI, API, and workforce — without timbal start managing them. You own process lifecycle, port allocation, env wiring, restarts, and log aggregation.
There is no timbal build or CLI-generated Docker image. You bring your own process manager (systemd, supervisord, Kubernetes, etc.) and production build pipeline.
Prerequisites
- A project created with
timbal create
- Bun for
api/ and ui/
- uv for workforce Python deps
timbal[server] installed in each workforce member’s venv
What timbal start does (that you must replicate)
timbal start is the reference implementation. When self-hosting, reproduce the same wiring:
1. Workforce members
For each workforce/<name>/ with a timbal.yaml:
cd workforce/<name>
uv sync
uv run -m timbal.server.http --port <port> --import_spec <fqn>
<fqn> comes from timbal.yaml (e.g. agent.py::agent). The HTTP server exposes:
POST http://localhost:<port>/run # collect (returns final OutputEvent)
POST http://localhost:<port>/stream # SSE event stream
GET http://localhost:<port>/healthcheck
2. API
cd api
bun install
PORT=<api_port> bun run dev # dev; use your production start command in prod
3. UI (if present)
cd ui
bun install
bun run dev --port <ui_port> # dev; use your production build/serve in prod
4. Cross-service env vars
The API and UI need to know where workforce members live. timbal start injects these — set them yourself when self-hosting:
| Variable | Example | Purpose |
|---|
TIMBAL_START_WORKFORCE | a1b2c3d4-...:4455,b5c6d7e8-...:4456 | Manifest _id → port map (comma-separated) |
TIMBAL_START_API_PORT | 3000 | API port (for services that call back to API) |
TIMBAL_START_UI_PORT | 3737 | UI port |
PORT | per-service | Port each Bun service binds to |
TIMBAL_START_WORKFORCE uses the _id from each member’s timbal.yaml, not the directory name.
Also pass through model keys and integration secrets (OPENAI_API_KEY, etc.). Locally, timbal start auto-loads <project>/.env and workforce/<name>/.env into each process; when self-hosting you must export or inject those variables yourself (e.g. via your process manager or a secrets store). See Environment variables.
If any of TIMBAL_START_* are missing, platform SDK helpers that resolve service URLs will fail. Mirror what timbal start sets before debugging routing issues.
Production considerations
Process management
Run each component under a supervisor that restarts on crash:
- One process per workforce member
- One for API
- One for UI (if used)
Use health checks against /healthcheck on workforce HTTP servers.
Logging
timbal start multiplexes stdout/stderr into one terminal with per-service prefixes and f/m focus controls. Self-hosted, you need your own approach:
- Separate log files or streams per component
- Structured logging if shipping to Datadog / CloudWatch / etc.
- Correlate by request ID if the API fans out to multiple workforce members
Networking
- Put a reverse proxy (nginx, Caddy, etc.) in front of UI and API for TLS
- Workforce members can stay internal — only the API needs to reach them
- Lock down ports so workforce HTTP servers aren’t exposed publicly
Builds
The scaffold uses bun run dev for API/UI. For production, use whatever production build your scaffolded api/ and ui/ packages define (static UI build + API server, etc.). The workforce side stays uv run -m timbal.server.http with a pinned uv sync environment.
Observability
Platform tracing still works from self-hosted runtimes — configure a tracing provider to export spans to Timbal or your own OTLP endpoint.
When to self-host
Self-hosting makes sense when you need full control over networking, data residency, or custom orchestration. For most teams, the Timbal Platform is less operational overhead — same project layout, no component wiring to maintain.