Core Concepts

4 min read
Rapid overview

Tiltfile & Tilt for Local Kubernetes Development

What is Tilt?

Tilt is a local Kubernetes development tool that watches your files, rebuilds containers, and updates your cluster in real-time. It uses a Tiltfile (written in Starlark, a Python dialect) to define your development environment.


Core Concepts

Basic Tiltfile Structure

# Tiltfile

# Load extensions
load('ext://restart_process', 'docker_build_with_restart')

# Define Kubernetes resources
k8s_yaml('kubernetes/deployment.yaml')

# Build Docker image and deploy
docker_build('myapp', '.')

# Configure resource behavior
k8s_resource('myapp', port_forwards='8080:80')

Key Functions

FunctionPurpose
docker_build()Build Docker image from Dockerfile
k8s_yaml()Apply Kubernetes manifests
k8s_resource()Configure a Kubernetes resource
local_resource()Run local commands (tests, builds)
helm()Deploy Helm charts

Docker Build Strategies

Basic Build

docker_build(
    'api-server',           # Image name
    '.',                    # Build context
    dockerfile='Dockerfile',
    build_args={'VERSION': '1.0.0'},
    ignore=['./tests', './docs'],
)

Live Update (Hot Reload)

Live Update syncs file changes without full image rebuilds.

docker_build(
    'frontend',
    '.',
    live_update=[
        # Sync local changes to container
        sync('./src', '/app/src'),
        sync('./public', '/app/public'),

        # Run command after sync
        run('npm install', trigger=['./package.json']),

        # Restart process if needed
        run('kill -HUP 1', trigger=['./src/**/*.ts']),
    ]
)

Live Update Strategies

# Sync files only (for interpreted languages)
live_update=[
    sync('./src', '/app/src'),
]

# Sync and run build (for compiled languages)
live_update=[
    sync('./src', '/app/src'),
    run('go build -o /app/server ./cmd/server'),
]

# Full restart (when config changes)
live_update=[
    sync('./config', '/app/config'),
    restart_container(),
]

Docker Build with Restart

load('ext://restart_process', 'docker_build_with_restart')

docker_build_with_restart(
    'api-server',
    '.',
    entrypoint='/app/server',
    live_update=[
        sync('./src', '/app/src'),
        run('go build -o /app/server ./cmd/server', trigger=['./src/**/*.go']),
    ],
)

Kubernetes Configuration

Loading Manifests

# Single file
k8s_yaml('deploy/app.yaml')

# Multiple files
k8s_yaml(['deploy/app.yaml', 'deploy/service.yaml'])

# Glob pattern
k8s_yaml(glob('deploy/*.yaml'))

# Kustomize
k8s_yaml(kustomize('deploy/overlays/dev'))

# Helm
k8s_yaml(helm('charts/myapp', values=['values-dev.yaml']))

Resource Configuration

k8s_resource(
    'api-server',
    port_forwards=['8080:80', '9090:9090'],
    resource_deps=['database', 'redis'],
    labels=['backend'],
    trigger_mode=TRIGGER_MODE_MANUAL,  # Don't auto-update
)

k8s_resource(
    'database',
    port_forwards='5432:5432',
    labels=['infrastructure'],
)

Resource Dependencies

# API depends on database being ready
k8s_resource('api-server', resource_deps=['database'])

# Frontend depends on API
k8s_resource('frontend', resource_deps=['api-server'])

Local Resources

Running Tests

local_resource(
    'unit-tests',
    cmd='npm test',
    deps=['./src', './test'],
    labels=['tests'],
    auto_init=False,      # Don't run on startup
    trigger_mode=TRIGGER_MODE_MANUAL,
)

local_resource(
    'integration-tests',
    cmd='npm run test:integration',
    resource_deps=['api-server', 'database'],
    labels=['tests'],
)

Build Steps

# Build frontend before Docker build
local_resource(
    'frontend-build',
    cmd='npm run build',
    deps=['./src', './package.json'],
    labels=['build'],
)

# Make docker_build depend on local build
docker_build(
    'frontend',
    '.',
    dockerfile='Dockerfile.prod',
    only=['./build'],
)
k8s_resource('frontend', resource_deps=['frontend-build'])

Helm Integration

Basic Helm Deploy

k8s_yaml(helm(
    'charts/myapp',
    name='myapp',
    namespace='default',
    values=['values.yaml', 'values-dev.yaml'],
    set=[
        'image.repository=myapp',
        'replicaCount=1',
    ]
))

Helm with Image Override

docker_build('myapp', '.')

k8s_yaml(helm(
    'charts/myapp',
    set=['image.repository=myapp', 'image.tag=latest']
))

Extensions

Loading Extensions

# From tilt-extensions repo
load('ext://restart_process', 'docker_build_with_restart')
load('ext://namespace', 'namespace_create')
load('ext://secret', 'secret_from_dict')

# Use namespace extension
namespace_create('my-namespace')

# Create secrets
secret_from_dict('db-secret', 'my-namespace', inputs={
    'username': 'admin',
    'password': os.getenv('DB_PASSWORD'),
})

Common Extensions

ExtensionPurpose
restart_processRestart container process without rebuild
namespaceCreate/manage namespaces
secretCreate Kubernetes secrets
git_resourceDeploy from Git repos
helm_remoteDeploy Helm charts from repos
configmapCreate ConfigMaps from files

Multi-Service Development

Microservices Setup

# Tiltfile for microservices

# Shared configuration
default_registry('localhost:5000')

# Database (external dependency)
k8s_yaml('infra/postgres.yaml')
k8s_resource('postgres', port_forwards='5432', labels=['infrastructure'])

# Redis
k8s_yaml('infra/redis.yaml')
k8s_resource('redis', port_forwards='6379', labels=['infrastructure'])

# API Gateway
docker_build('api-gateway', './services/gateway')
k8s_yaml('services/gateway/k8s.yaml')
k8s_resource('api-gateway',
    port_forwards='8080',
    resource_deps=['postgres', 'redis'],
    labels=['services'])

# User Service
docker_build('user-service', './services/user',
    live_update=[
        sync('./services/user/src', '/app/src'),
    ])
k8s_yaml('services/user/k8s.yaml')
k8s_resource('user-service',
    resource_deps=['postgres'],
    labels=['services'])

# Order Service
docker_build('order-service', './services/order')
k8s_yaml('services/order/k8s.yaml')
k8s_resource('order-service',
    resource_deps=['postgres', 'redis'],
    labels=['services'])

Shared Libraries

# Watch shared library and rebuild dependents
watch_file('./shared')

docker_build('service-a', './services/a',
    deps=['./shared'])
docker_build('service-b', './services/b',
    deps=['./shared'])

Advanced Patterns

Conditional Configuration

# Development vs staging
config.define_string('environment', args=True)
cfg = config.parse()
env = cfg.get('environment', 'dev')

if env == 'dev':
    k8s_yaml(kustomize('deploy/overlays/dev'))
    allow_k8s_contexts('docker-desktop')
else:
    k8s_yaml(kustomize('deploy/overlays/staging'))
    allow_k8s_contexts('staging-cluster')

Custom Buttons

k8s_resource('api-server',
    port_forwards='8080',
    links=[
        link('http://localhost:8080/swagger', 'Swagger UI'),
        link('http://localhost:8080/health', 'Health Check'),
    ],
)

# Custom action button
local_resource(
    'seed-database',
    cmd='./scripts/seed.sh',
    auto_init=False,
    trigger_mode=TRIGGER_MODE_MANUAL,
)

Resource Groups

# Use labels to organize in UI
k8s_resource('frontend', labels=['frontend'])
k8s_resource('api-server', labels=['backend'])
k8s_resource('worker', labels=['backend'])
k8s_resource('postgres', labels=['database'])
k8s_resource('redis', labels=['cache'])

Performance Optimization

Ignore Unnecessary Files

docker_build('myapp', '.',
    ignore=[
        './node_modules',
        './.git',
        './tests',
        './docs',
        '**/*.md',
        '**/*.test.ts',
    ])

Only Include Needed Files

docker_build('frontend', '.',
    only=[
        './build',
        './package.json',
    ])

Parallel Builds

# Tilt runs builds in parallel by default
# Use resource_deps to control order when needed

docker_build('service-a', './a')  # Parallel
docker_build('service-b', './b')  # Parallel
docker_build('service-c', './c')  # Parallel

# Only service-d waits
docker_build('service-d', './d')
k8s_resource('service-d', resource_deps=['service-a', 'service-b'])

Debugging

Tilt Commands

# Start Tilt
tilt up

# Start with specific resources
tilt up frontend api-server

# Start in CI mode (exit on completion)
tilt ci

# View Tilt UI
tilt up  # Opens at http://localhost:10350

# Get resource status
tilt get
tilt describe <resource>

# Trigger manual rebuild
tilt trigger <resource>

# View logs
tilt logs <resource>

Debugging Tiltfile

# Print debug info
print("Building with config:", cfg)

# Fail with message
fail("Missing required environment variable")

# Local commands for debugging
local('kubectl get pods')

Interview Questions

1. What problem does Tilt solve?

Tilt eliminates the slow inner development loop of:

  1. Make code change
  2. Build Docker image
  3. Push to registry
  4. Update Kubernetes deployment
  5. Wait for pod rollout
  6. Test change

With Tilt's live update, changes sync in seconds.

2. Explain Live Update vs Full Rebuild

Live Update:

  • Syncs changed files directly to running container
  • No image rebuild needed
  • Sub-second updates
  • Best for interpreted languages (Python, Node.js)

Full Rebuild:

  • Complete Docker build cycle
  • Required when dependencies change
  • Slower but ensures clean state

3. How do resource dependencies work?

resource_deps ensures resources start in order:

  • Dependent resource waits for dependencies to be "ready"
  • Ready = Kubernetes reports resource as available
  • Prevents startup race conditions

4. Best practices for Tiltfile organization?

  1. Group related resources with labels
  2. Use extensions for common patterns
  3. Configure ignore to skip unnecessary rebuilds
  4. Use live_update for fast iteration
  5. Set appropriate trigger_mode for expensive operations
  6. Document custom configurations with comments