Skip to main content
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:
VariableExamplePurpose
TIMBAL_START_WORKFORCEa1b2c3d4-...:4455,b5c6d7e8-...:4456Manifest _id → port map (comma-separated)
TIMBAL_START_API_PORT3000API port (for services that call back to API)
TIMBAL_START_UI_PORT3737UI port
PORTper-servicePort 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.