Big News!  Tilt is joining Docker

Getting Started with Image Builds

This is a brief intro to automatically build and deploy images to a local dev environment.

We’ll start with a single image.

Going from docker build to docker_build

On the command-line, an image build looks like this:

docker build -t my-image .

my-image is the image name. . is the directory of files that you’re including in the image.

To make this part of your dev environment, add this to your Tiltfile:

docker_build('my-image', '.')

. is still the directory of files you’re including. Tilt will automatically watch them for changes.

my-image is an image selector. Tilt will scan all your workload manifests for images, and match any object that contains the image name my-image (regardless of tag).

This creates a dependency between your workload and my-image. Every time the image rebuilds, Tilt knows it may need to restart your containers.

  • If the image hasn’t changed, the containers won’t restart.

  • If the image has changed, the containers should always restart with a fresh image.

(Note that this is different than what usually happens in prod, where you want more control over the gradual rollout of a new image. Tilt has some tricks to make this work well in dev. The implementation details are discussed a bit in our custom_build guide.)

Advanced Image Matching

my-image is a matcher that ignores tags.

Some teams have one mega image with all their services, and use tags to denote which service to run.

docker_build('my-mega-image:service-a', '.', entrypoint='/service-a')
docker_build('my-mega-image:service-b', '.', entrypoint='/service-b')

If you specify a tag, Tilt will only match deploy objects with that exact tag in their image name.

If you’re trying to inject images into Custom Resources (in other words, if you’re not using built-in Kubernetes objects like Deployment and Job), check out our custom resource guide.

Multiple Images

When you start adding multiple services to your app, it’s easiest to just copy a new Dockerfile for each service and tweak a few parameters.

Once you have a few services, you may find that duplication can start to feel messy. Sometimes you update one Dockerfile but forget to update the others. Building all the services is slow.

A common pattern is to create a common base image with the dependencies for multiple services.

So let’s set up a dependency tree of docker_builds that build on each other.

Building a node_modules Base Image

Let’s look at an example. We want to create a NodeJS server with two Docker images:

  1. A Docker image that contains the node_module dependencies.

  2. A Docker image that contains the server source code.

First we write a Dockerfile for the base node_modules image. We’ll call this image nodejs-express-base-image.

# nodejs-express-base-image
# base.dockerfile

FROM node:16-alpine

# Default value; will be overridden by build_args, if passed
ARG node_env=production

ENV NODE_ENV $node_env

WORKDIR '/var/www/app'
ADD package.json package.json
RUN npm install
ENTRYPOINT node server.js

Next we’ll create a Dockerfile for the app image. This is just an empty base image to build on.

# nodejs-express-app-image
# app.dockerfile

FROM nodejs-express-base-image

WORKDIR '/var/www/app'

ADD . .

Lastly, we’ll add a Tiltfile that knows how to build both images.


# Set up the Kubernetes resources.
k8s_yaml('app.yml')

# Configure image build for our external dev dependencies.
docker_build('nodejs-express-base-image',
             './package',
             dockerfile='base.dockerfile',
             build_args={'node_env': 'development'})

# Configure build to copy our source code.
docker_build('nodejs-express-app-image',
             '.',
             dockerfile='app.dockerfile')
             
# Configure the Kubernetes deploys.
k8s_resource('nodejs-express-app', port_forwards=3000)

Notice that the Docker build for nodejs-express-deps uses the subdirectory ./package. Tilt will only rebuild this image when files under ./package change.

When you run tilt up, Tilt will build both images, and make sure that the first image gets injected into the second image.

STEP 1/5 — Building Dockerfile: [nodejs-express-base-image]
Building Dockerfile:
  FROM node:16-alpine
  
  # Default value; will be overridden by build_args, if passed
  ARG node_env=production
  
  ENV NODE_ENV $node_env
  
  WORKDIR '/var/www/app'
  ADD package.json package.json
  RUN npm install
  ENTRYPOINT node server.js

...

STEP 3/5 — Building Dockerfile: [nodejs-express-app-image]
Building Dockerfile:
  FROM localhost:5005/nodejs-express-base-image:tilt-19328501fd376562
  
  WORKDIR '/var/www/app'
  
  ADD . .


     Tarring context…
     Building image
     copy /context / [done: 44ms]
     [1/3] FROM localhost:5005/nodejs-express-base-image:tilt-19328501fd376562
     [2/3] WORKDIR /var/www/app [cached]
     [3/3] ADD . . [done: 18ms]
     exporting to image [done: 21ms]

If you make a change to server.js, Tilt knows it can skip the first image build and just do the second.

Adding Live Updates

Once you’ve got the two image builds working, you can add a live update rule to sync files into your app. This is much faster than building the app image each time.

In this example, we’ll use a sync step to copy the files.

Then we’ll add a custom entrypoint that runs our server with nodemon, which does the reload.

Here’s what it looks like:


docker_build('nodejs-express-app-image',
             '.',
             dockerfile='app.dockerfile',
             live_update=[
               sync('.', '/var/www/app')
             ],
             entrypoint='yarn run nodemon /var/www/app/server.js')

Every app needs to specify both a sync step and a reload step. But reload steps tend to be specific to the programming language and framework you’re using. Some frameworks even handle it automatically. This example uses an entrypoint with nodemon, but the reload step for your project will probably look different. For more examples, see the language-specific example projects or the live update reference.

Note that live update steps should always be attached to the deployed image, never the base image. Tilt’s live update system matches the image in the container, so needs to be attached to the deployed image to figure out which container to update.

Troubleshooting

If you’re confused about an image’s build context or seeing rebuilds you don’t expect, the debugging file changes guide gives a thorough breakdown of how Tilt watches and ignores files, and the live update reference will walk you through the relationship between an image’s build context and live_update rules.

Example Code