Live-Reloading Kubernetes Workloads With Tilt
Every code change in the local cluster costs 30–60 seconds — docker build, push or load, kubectl rollout, wait for the new pod — and developers start batching changes or moving back to running services outside the cluster, which defeats the point of having one. Or Tilt is configured, but every save still triggers a full image build because live_update never matches. This page configures Tilt so source changes are synced into running containers in about two seconds, while dependency changes still trigger proper rebuilds. It is part of local Kubernetes development.
Tilt reads a Tiltfile written in Starlark (a Python dialect), watches the files each image depends on, and decides per change whether to rebuild the image or apply a live_update — a set of file syncs and commands run inside the existing container.
Diagnostic
Check how Tilt handled the most recent change and how long it took. The CLI exposes the same data as the web UI:
#!/usr/bin/env bash
set -euo pipefail
touch api/src/routes/health.ts
sleep 8
tilt get uiresource api -o json | jq -r '.status.buildHistory[0] | "\(.startTime) \(.finishTime) error=\(.error // "none")"'
tilt logs api --since 30s | grep -E 'Building|Step|live update|Will not perform' | head -8
Expected bad output when live update is not taking effect:
2026-09-18T10:02:11Z 2026-09-18T10:02:49Z error=none
Building Dockerfile for platform linux/arm64:
Step 1 - 4.61s (Building Dockerfile)
Step 2 - 21.30s (Pushing localhost:5001/api:tilt-9c1f)
Will not perform Live Update because: Found file(s) not matching any sync (files: [api/src/routes/health.ts])
A 38-second full rebuild, and Tilt names the reason: the changed file did not match any sync path.
Root cause
live_update only runs when every changed file matches a sync source path in that image's live_update steps. The paths are resolved relative to the Tiltfile, and the most common mistakes are mismatched roots (sync('./src', ...) when the code lives in ./api/src), a docker build context that includes files the sync rules do not cover (tests, config files next to the source), and a container path that does not match where the Dockerfile copies the code. When any changed file falls outside the rules, Tilt falls back to a full rebuild — correctly, because it cannot know how to apply that file. The second failure mode is the opposite: files synced into the container that need a rebuild to take effect, such as a new dependency in package.json, which leaves the container running with stale node_modules.
Push time dominates in the diagnostic because the image is pushed to a registry each time. For source-only changes that cost disappears with live update, which is why getting the sync rules right matters more than any registry tuning.
Resolution
- Align sync paths with the Dockerfile's
COPYlayout. If the Dockerfile doesCOPY src /app/src, the sync must map the same directory:
docker_build(
'localhost:5001/api',
context='./api',
dockerfile='./api/Dockerfile',
ignore=['./api/test', './api/**/*.md'],
live_update=[
fall_back_on(['./api/package.json', './api/package-lock.json', './api/Dockerfile']),
sync('./api/src', '/app/src'),
sync('./api/config', '/app/config'),
run('kill -USR2 1', trigger=['./api/config']),
],
)
ignore removes files from the build context and from watching, so edits to tests no longer force rebuilds of the service image. fall_back_on lists files that must always trigger a full rebuild.
- Run the process under a watcher inside the container so synced files are picked up. For Node,
node --watchornodemonrestarts on change; many frameworks already reload in development mode:
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY src ./src
COPY config ./config
CMD ["node", "--watch", "src/server.js"]
- For compiled languages, compile on the host and sync the binary. Syncing Go source into a container that would then need a compiler defeats the purpose. Build with
local_resource, then sync the output and restart:
local_resource(
'worker-compile',
'CGO_ENABLED=0 GOOS=linux go build -o ./worker/build/worker ./worker/cmd/worker',
deps=['./worker/cmd', './worker/internal'],
labels=['backend'],
)
docker_build(
'localhost:5001/worker',
context='./worker',
dockerfile='./worker/Dockerfile.dev',
only=['./build'],
live_update=[sync('./worker/build/worker', '/app/worker'), run('kill -HUP 1')],
)
Set GOARCH to match the cluster's node architecture when it differs from the host.
- Group resources and expose ports so the UI is usable:
k8s_yaml(helm('./charts/shop', values=['./charts/shop/values-local.yaml']))
k8s_resource('api', port_forwards='8080:8080', labels=['backend'], resource_deps=['postgres'])
k8s_resource('worker', labels=['backend'], resource_deps=['worker-compile'])
k8s_resource('postgres', labels=['infra'])
Expected output
$ touch api/src/routes/health.ts && sleep 4 && tilt logs api --since 10s | grep -iE 'live update|synced'
Will copy 1 file(s) to container: [api-7c9d5f8b6d-x2kqp/api]
[Live Update] Syncing api/src/routes/health.ts → /app/src/routes/health.ts
[Live Update] Done in 1.6s
api │ Restarting 'src/server.js'
The file is copied into the running container in under two seconds and Node's watcher restarts the process. Changing package.json still triggers a full rebuild, as intended.
Prevention
Keep the Tiltfile next to the Dockerfiles it mirrors and review both together; a
COPYpath change in a Dockerfile without the matchingsyncchange is the usual regression.Check for drift before debugging. When behaviour differs from CI, compare the running container's files with the working tree:
kubectl exec deploy/api -- sh -c 'cd /app && find src -type f -newer package.json | head'shows synced files, andtilt trigger apiforces a clean rebuild to rule out live-update drift.Run
tilt ciin CI for the same Tiltfile. It brings up every resource, waits for them to be healthy and exits, which catches broken Tiltfiles the day they break rather than on the next developer's machine.
Platform caveats
Apple Silicon (ARM64): Tilt builds for the cluster's architecture automatically when using kind on the same machine. For
local_resourcebuilds of compiled binaries, setGOARCH=arm64explicitly, or the synced binary fails withexec format error.
macOS (Docker Desktop): Tilt's file watching runs on the host, so there are no bind-mount inotify issues; syncing uses
kubectl cp-like tar streams that are unaffected by virtiofs performance.
WSL2: keep the repository inside the WSL filesystem, not under
/mnt/c. Tilt's watcher does not receive file events reliably from Windows-mounted drives, which makes live update appear to do nothing.
Rollback
Remove the live_update argument to return to full rebuilds on every change; nothing else depends on it:
#!/usr/bin/env bash
set -euo pipefail
git restore Tiltfile
tilt down
tilt up
Frequently Asked Questions
Why does Tilt say "Found file(s) not matching any sync"?
At least one changed file is outside every sync source path in that image's live_update. Add a sync for its directory, or exclude it from the build context with ignore or only if the image does not need it.
Is live update safe to use for everything?
It is safe for development, but the container gradually diverges from what its image would contain. Rebuild from scratch when switching branches or when something works locally but fails in CI.
How does Tilt compare with Skaffold's file sync?
Both sync files into running containers. Tilt's rules are expressed in Starlark with explicit fallbacks and triggers, and its UI shows why each rebuild happened. Skaffold is declarative YAML and integrates closely with its CI deploy pipeline. Pick one and use it consistently.
Do I still need a registry with Tilt?
For kind, Tilt can load images directly without one, but it detects a local registry through the local-registry-hosting ConfigMap and pushes there instead, which is faster for large images and multi-node clusters.