Install
# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash
# macOS via Homebrew
brew install tilt-dev/tap/tilt
# Or via mise (see /tutorials/mise-polyglot-runtime-versions.html)
mise use -g tilt@latest
tilt version
Tilt needs a Kubernetes cluster — not your production one. Recommended local options:
- kind — lightweight K8s in Docker.
kind create cluster. - k3d — K3s in Docker.
k3d cluster create dev. - Docker Desktop / Rancher Desktop — both bundle a K8s cluster.
- minikube — the original; heavier than kind or k3d.
Tilt detects your current kubectl context and refuses to run against a context with "prod" / "production" / "live" in its name without an explicit allow.
The first Tiltfile
A Tiltfile is a Starlark (Python-ish) script in the project root that describes how to build, deploy, and watch your services:
# Tiltfile
load('ext://restart_process', 'docker_build_with_restart')
# Build a Docker image; rebuild when source changes
docker_build(
'myapp/api',
'./services/api',
dockerfile='./services/api/Dockerfile',
live_update=[
sync('./services/api/src', '/app/src'),
run('npm install --silent', trigger=['./services/api/package.json']),
restart_container(),
],
)
# Apply Kubernetes manifests for this service
k8s_yaml('./services/api/k8s.yaml')
# Expose the service on a local port
k8s_resource('api', port_forwards='8080:8080')
# Same for the web frontend
docker_build('myapp/web', './services/web')
k8s_yaml('./services/web/k8s.yaml')
k8s_resource('web', port_forwards='3000:3000')
# A local helper command — run a one-off database migration
local_resource(
'db-migrate',
'kubectl exec -it deploy/api -- npm run migrate',
auto_init=False, # don't run on tilt up; trigger manually
deps=['./services/api/migrations'],
)
Launch:
tilt up
Tilt opens a web UI at http://localhost:10350/ showing every resource: build status, logs, restart history, port forwards. tilt down tears it all back down.
Live update: the killer feature
By default, Tilt rebuilds the Docker image and rolls the deployment on every change. That's correct but slow (image rebuild + push + pull + restart, ~30-60s).
Live update short-circuits this for code changes: instead of rebuilding, sync the changed files into the running container and (optionally) restart the process inside it — no image rebuild, no pod restart. Sub-2-second turnaround for most language stacks.
docker_build(
'myapp/api',
'./services/api',
live_update=[
# Sync files into the running container
sync('./services/api/src', '/app/src'),
# On dependency change, run npm install inside the container
run('npm install --silent', trigger=['./services/api/package.json']),
# After syncing, restart the Node process
restart_container(),
# Falls back to a full rebuild for changes outside the live-update scope
],
)
For framework-aware hot-reload (Next.js, Vite, Spring Boot DevTools, Flask debug), the same pattern but skip restart_container — the framework picks up the synced files itself.
The UI
Open http://localhost:10350:
- Left sidebar: every resource. Green = ready, yellow = building, red = error.
- Center: logs from the selected resource (with auto-scroll, search, level filter).
- Top: aggregate health (count of failing services), runtime since
tilt up. - Per-resource actions: trigger rebuild, copy port-forward URL, show YAML, exec into pod.
For a 10-service app, this collapses "look at deployment status, then logs for that one, then ports for the other" into one screen.
Useful Tiltfile patterns
# Conditional: only build a heavy service if explicitly requested
config.define_string_list('to-run', args=True)
cfg = config.parse()
to_run = cfg.get('to-run', ['api', 'web'])
if 'analytics' in to_run:
docker_build('myapp/analytics', './services/analytics')
k8s_yaml('./services/analytics/k8s.yaml')
# tilt up -- analytics starts just analytics
# Use Helm to render manifests
yaml = helm('./charts/myapp', name='myapp',
values=['./charts/myapp/values.dev.yaml'])
k8s_yaml(yaml)
# Use Kustomize
k8s_yaml(kustomize('./k8s/overlays/dev'))
# Local resource that runs even without a cluster (DB setup, codegen)
local_resource(
'codegen',
'go generate ./...',
deps=['./internal/schema'],
)
# Resource ordering via dependencies
k8s_resource('api', resource_deps=['postgres', 'redis'])
k8s_resource('web', resource_deps=['api'])
# Custom labels / grouping
k8s_resource('api', labels=['backend'])
k8s_resource('postgres', labels=['data'])
Skaffold + Devspace: the alternatives
- Skaffold — Google's K8s dev tool. Similar inner-loop story; YAML config instead of Starlark; integrates tightly with Cloud Code / IntelliJ. For teams already in the GCP ecosystem, often the easier fit.
- DevSpace — YAML-based with strong "dev container running in cluster" pattern (mount your laptop's local dir into a pod, edit live).
- Garden — more declarative; better for shared dev clusters with policy.
All three solve the same problem. Tilt has the smallest config surface for "I just want to iterate fast" cases; Skaffold has the biggest ecosystem; DevSpace pioneered the "edit live in the pod" pattern that the others now also support.
When Tilt is overkill
- If you have a single service that you can run locally with
npm run dev/cargo run/python manage.py runserver, just do that. Tilt earns its complexity once there are 3+ services that need to talk to each other. - If you're targeting non-Kubernetes deploys (plain Docker / VMs / lambda), Tilt isn't the right shape.
- For pure-frontend dev,
vite devwith a hot-reload server beats Tilt-in-the-loop; use Tilt for the backend services and hit them from the standalone Vite dev server.
For "we deploy to Kubernetes in prod, and we want dev to feel that way too without paying the build-and-apply tax on every edit," Tilt is the most ergonomic option in 2026.