Tilt and Skaffold are both fantastic tools for Kubernetes development, but they approach the problem from slightly different angles.

The most surprising thing about migrating from Skaffold to Tilt is how much less you have to think about.

Let’s see Tilt in action. Imagine you have a simple Go web server and a Kubernetes deployment for it.

Here’s our main.go:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello from Tilt!")
}

func main() {
	http.HandleFunc("/", handler)
	log.Println("Starting server on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

And here’s our k8s/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-web-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-web-app
  template:
    metadata:
      labels:
        app: my-web-app
    spec:
      containers:
      - name: web
        image: my-web-app:latest # We'll build this!
        ports:
        - containerPort: 8080

Now, the magic happens in Tiltfile. This is Tilt’s configuration.

# Tiltfile
docker_build(
    image_name='my-web-app',
    build_context='.',
    dockerfile='Dockerfile'
)

k8s_resource(
    name='my-web-app-deployment',
    yaml_file='k8s/deployment.yaml'
)

# This is the key part for live updates!
# It tells Tilt how to sync files and restart the container.
live_update(
    'my-web-app', # The image name from docker_build
    'web',        # The container name from your k8s manifest
    'main.go',    # The file to watch
    'main.go'     # The destination in the container
)

And a minimal Dockerfile:

FROM golang:1.20-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/main
CMD ["/app/main"]

When you run tilt up, Tilt will:

  1. Build the Docker image: It sees docker_build and executes it.
  2. Apply Kubernetes manifests: It sees k8s_resource and applies k8s/deployment.yaml.
  3. Watch for changes: It notices live_update('main.go', 'main.go').

Now, edit main.go. Change "Hello from Tilt!" to "Hello from Tilt, again!". Save the file.

Tilt’s UI will update, showing a build happening. Then, it will automatically sync main.go into the running container and restart the web process. Refresh your browser pointing to the service, and you’ll see the updated message. No full pod restarts, no waiting for docker build to finish from scratch.

Skaffold usually requires you to define a build step and a deploy step, and when you change code, it triggers a full rebuild of the image and a redeploy of the Kubernetes resource. Tilt’s live_update is a more granular approach. It focuses on just syncing the changed file and restarting the specific process within the container. This is incredibly fast for interpreted languages or compiled languages where only specific files change.

The core mental model for Tilt revolves around three main concepts: Build, Deploy, and Live Update.

  • Build: How to create your container images. This is typically docker_build or buildah_build. You define the image name, build context, and Dockerfile.
  • Deploy: How to get your application running in Kubernetes. This is usually k8s_resource pointing to your YAML files. Tilt can also manage Helm charts with helm_resource.
  • Live Update: This is where Tilt shines. It allows you to define specific files or directories to sync into a running container and commands to execute to pick up those changes. This bypasses the need for a full image rebuild and pod restart for many common development workflows. You map local files to remote paths within the container and specify a restart command.

The exact levers you control are within the Tiltfile. You can:

  • Define multiple docker_build steps: For microservices with different images.
  • Specify build arguments and environment variables for builds: docker_build(..., args=['VERSION=1.2'], env={'MY_VAR': 'value'}).
  • Configure Kubernetes resources: Including namespaces, labels, and more granular control over how Tilt applies them.
  • Tailor live_update: You can sync entire directories, use glob patterns, and define more complex restart commands. For Go, live_update('main.go', 'main.go', 'go run /app/main.go') might be used if you’re not building a binary first. For Node.js, it might be syncing *.js files and running npm restart.
  • Set up port forwarding: port_forward(8080, 8080) to make local access easy.
  • Create custom UI elements: Tilt’s UI is highly configurable.

Most people understand docker_build and k8s_resource, but the real power comes from configuring live_update for different languages and workflows. For instance, for a Python application, you might sync the entire application directory and use a command like sh -c "pip install -r requirements.txt && python app.py" to ensure dependencies are reinstalled and the app restarts. This level of dynamic control over the container’s lifecycle during development is what differentiates Tilt.

The next step is often understanding how to manage multi-service applications and their dependencies within Tilt.

Want structured learning?

Take the full Skaffold course →