Tilt is an open source tool that watches your source code, rebuilds container images, applies Kubernetes manifests and syncs changes into running pods, all from a single Tiltfile. Instead of running docker build, kind load and kubectl apply by hand after every edit, you save a file and see the result in seconds. In this tutorial you will install Tilt on Ubuntu 24.04, create a local cluster with kind, and build a small Python app backed by Redis with live updates, port forwarding and startup ordering.
Prerequisites
To follow this tutorial, you will need:
- A machine running Ubuntu 24.04 LTS, either your workstation or a CubePath VPS, with at least 2 CPUs and 4 GB of RAM.
- A non-root user with
sudoprivileges. - Docker Engine installed from Docker's official repository, with your user in the
dockergroup so you can rundockerwithoutsudo. kubectlinstalled. The quickest way on Ubuntu issudo snap install kubectl --classic.- Basic familiarity with Kubernetes Deployments and Services.
Confirm Docker works for your user before continuing:
docker run --rm hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Step 1 - Creating a local cluster with kind
Tilt needs a Kubernetes cluster to deploy to. kind (Kubernetes in Docker) runs a full cluster inside a Docker container, and Tilt detects it automatically and loads your images straight into it, so you do not need a registry.
Download the latest kind binary and install it to /usr/local/bin:
curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/latest/download/kind-linux-amd64
sudo install -m 0755 ./kind /usr/local/bin/kind
rm ./kind
On an ARM64 machine, replace kind-linux-amd64 with kind-linux-arm64.
Create a cluster named tilt-dev:
kind create cluster --name tilt-dev
kind switches your kubectl context to the new cluster. Check that the node is ready:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
tilt-dev-control-plane Ready control-plane 45s v1.33.1
Step 2 - Installing Tilt
Tilt publishes an install script that detects your architecture, downloads the matching release and places the tilt binary on your PATH. Download it first so you can read what it does before running it:
curl -fsSL -o install-tilt.sh https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh
less install-tilt.sh
When you are satisfied, run it and remove the script:
bash install-tilt.sh
rm install-tilt.sh
Verify the installation:
tilt version
v0.35.2, built 2025-09-30
Your version number will differ. Any recent release works with this tutorial.
Step 3 - Creating a sample application
To see Tilt in action you need something to build. Create a project directory with a small Flask app that counts page visits in Redis:
mkdir -p ~/tilt-demo/k8s
cd ~/tilt-demo
Create the application file:
nano app.py
import os
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379)
@app.route("/")
def index():
visits = redis.incr("visits")
return f"Hello from Tilt! This page has been viewed {visits} times.\n"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
List the Python dependencies:
nano requirements.txt
flask==3.1.*
redis==6.*
Now write the Dockerfile. Dependencies are installed before the source is copied, so Docker can cache that layer:
nano Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
Next, create the Kubernetes manifests. The app Deployment references the image tilt-demo. Tilt replaces that name with the exact image it builds, so you never have to manage tags yourself:
nano k8s/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tilt-demo
spec:
replicas: 1
selector:
matchLabels:
app: tilt-demo
template:
metadata:
labels:
app: tilt-demo
spec:
containers:
- name: app
image: tilt-demo
ports:
- containerPort: 8000
env:
- name: REDIS_HOST
value: redis
---
apiVersion: v1
kind: Service
metadata:
name: tilt-demo
spec:
selector:
app: tilt-demo
ports:
- port: 8000
targetPort: 8000
Then create a Redis Deployment and Service for the app to talk to:
nano k8s/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
The readiness probe matters: Tilt waits for a pod to be ready before it marks the resource as healthy, and you will use that in the next step to start the app only after Redis is up.
Step 4 - Writing your first Tiltfile
The Tiltfile lives in the project root and is written in Starlark, a small dialect of Python. It tells Tilt what to build, what to deploy and how to expose it. Create it:
nano Tiltfile
# Refuse to run against anything other than the local kind cluster
allow_k8s_contexts('kind-tilt-dev')
# Build the app image from the Dockerfile in this directory
docker_build('tilt-demo', '.')
# Deploy the manifests
k8s_yaml(['k8s/redis.yaml', 'k8s/app.yaml'])
# Group resources in the UI, forward the app port and start it after Redis
k8s_resource('redis', labels=['infra'])
k8s_resource(
'tilt-demo',
port_forwards='8000:8000',
resource_deps=['redis'],
labels=['app'],
)
A few points about this file:
allow_k8s_contexts()is a safety net. Tilt already refuses to deploy to contexts that do not look local, and this line makes the intent explicit.k8s_resource()names match the Deployment names in the manifests (redisandtilt-demo).resource_deps=['redis']makes Tilt wait until the Redis pod passes its readiness probe before deploying the app.
Start Tilt from the project directory:
tilt up
Tilt started on http://localhost:10350/
v0.35.2, built 2025-09-30
(space) to open the browser
(s) to stream logs (--stream=true)
(t) to open legacy terminal mode (--legacy=true)
(ctrl-c) to exit
Press the space bar to open the web UI, or browse to http://localhost:10350. You will see the redis resource come up first, followed by tilt-demo. In a second terminal, test the app through the port forward:
curl http://localhost:8000
Hello from Tilt! This page has been viewed 1 times.
NoteTilt binds its UI and port forwards to
localhost. If you run Tilt on a remote server, open an SSH tunnel from your workstation instead of exposing the ports publicly:ssh -L 10350:localhost:10350 -L 8000:localhost:8000 your_user@your_server_ip.
Step 5 - Enabling live updates
Right now, every change to app.py triggers a full image rebuild and a new pod. That works, but it takes several seconds. Live update copies changed files into the running container instead, and then restarts the process.
The built-in restart_container() step is deprecated for Kubernetes, so the supported way to restart the process is the restart_process extension. Stop Tilt with Ctrl+C, then replace the docker_build(...) line in the Tiltfile with the following:
load('ext://restart_process', 'docker_build_with_restart')
docker_build_with_restart(
'tilt-demo',
'.',
entrypoint=['python', 'app.py'],
live_update=[
# Rebuild the whole image if dependencies change
fall_back_on(['requirements.txt']),
# Otherwise, copy the source file into the running container
sync('./app.py', '/app/app.py'),
],
)
docker_build_with_restart wraps your entrypoint so it can be restarted after each sync. fall_back_on() tells Tilt that some changes (here, new dependencies) cannot be synced and need a full rebuild. sync() paths are local path first, container path second, and the local path must be inside the build context.
Start Tilt again:
tilt up
Edit the greeting in app.py, for example change Hello from Tilt! to Hello again!, and save. In the Tilt UI the tilt-demo resource shows a live update instead of an image build, and it completes in about a second. Confirm the change:
curl http://localhost:8000
Hello again! This page has been viewed 2 times.
The counter kept going because only the app process restarted: the pod and the Redis data were not touched.
Step 6 - Adding local tasks
Tilt can also run commands on your machine as part of the dev loop, such as code generation, linters or tests. A local_resource runs a command whenever the files in deps change. Add this to the end of the Tiltfile:
local_resource(
'syntax-check',
cmd='python3 -m py_compile app.py',
deps=['app.py'],
labels=['checks'],
)
Tilt reloads the Tiltfile automatically when you save it. A new syntax-check resource appears in the UI and turns red if you save app.py with a syntax error, before the broken code ever reaches the cluster.
For tasks you only want to run on demand, such as seeding a database, add auto_init=False and trigger_mode=TRIGGER_MODE_MANUAL to the local_resource call and start it with the trigger button in the UI or from the CLI:
tilt trigger syntax-check
Step 7 - Inspecting and debugging resources
Most debugging happens in the web UI, where each resource has its own log pane, build history and error highlighting. The same information is available from the command line while tilt up is running.
Show the logs of one resource:
tilt logs tilt-demo
Force a rebuild and redeploy of a resource, for example after changing something Tilt does not watch:
tilt trigger tilt-demo
Open a shell in the running app container with plain kubectl:
kubectl exec -it deploy/tilt-demo -- /bin/sh
When you are done for the day, stop Tilt with Ctrl+C and remove everything it deployed:
tilt down
Step 8 - Running the same setup in CI
tilt ci runs the same Tiltfile non-interactively: it builds and deploys every resource, streams the logs, and exits with code 0 once all resources are healthy, or non-zero if any of them fails. This lets you reuse your dev environment as an integration test:
tilt ci
...
SUCCESS. All workloads are healthy.
Run tilt down afterwards to clean up the cluster.
Troubleshooting
Port 10350 is already in use. Another Tilt instance is probably running. Stop it, or start this one on a different port with tilt up --port 10351.
The pod is stuck in ErrImagePull or ImagePullBackOff. The cluster is trying to pull tilt-demo from a registry instead of using the image Tilt built. Make sure kubectl config current-context returns kind-tilt-dev so Tilt recognizes the kind cluster and loads the image into it.
Tilt says "Stop! ... might be production". Your current kubectl context points at a remote cluster that Tilt does not consider local. Switch back with kubectl config use-context kind-tilt-dev instead of adding the remote context to allow_k8s_contexts().
Live update does not trigger. The local path in sync() must be inside the docker_build context and must match the files you edit. Check the resource logs in the UI: Tilt prints which files changed and whether it synced them or fell back to a full build.
Conclusion
You installed Tilt and kind on Ubuntu 24.04 and wrote a Tiltfile that builds an image, deploys two services in the right order, forwards a port, syncs code changes into a running pod and runs a local check on every save. The same file also works as an integration test with tilt ci.
As next steps, you can:
- Replace the plain manifests with a Helm chart using
k8s_yaml(helm('path/to/chart')). - Split a larger project into several Tiltfiles and combine them with
include(). - Browse the extensions at
https://github.com/tilt-dev/tilt-extensionsfor ready-made helpers such ashelm_resourceandnamespace.
