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 sudo privileges.
  • Docker Engine installed from Docker's official repository, with your user in the docker group so you can run docker without sudo.
  • kubectl installed. The quickest way on Ubuntu is sudo 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 (redis and tilt-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.

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-extensions for ready-made helpers such as helm_resource and namespace.