Three Image Builders to Try While You're Waiting on 'docker build' to Finish

A guide to Tilt extensions that build images differently

Two things can be true at the same time:

  • If you don’t want to worry about containers, Docker is a great one-stop-shop for building, moving, storing, and running container images.

  • Container nerds get frustrated with this, because bundling all these things together means you often get a slow tool that isn’t particularly well-optimized for what you’re doing.

Jason Hall recently tweeted about this, then wrote a great blog post expanding on it.

Teams using Tilt often ask us: if my Docker build is getting slow, how can Tilt help me make it faster?

Fortunately, Tilt lets you swap out and experiment with different image builders in dev. And you can package up those experiments as extensions. The Tilt community frequently adds new types of image builders.

In this post, I want to brag about three very different types of image builders that exist today:

  • kubectl_build,

  • ko, and

  • pack

We’ll talk about why you’d want to use them, how to swap them out, and the pros/cons of each!

If you just want to see the code, it’s here: https://github.com/tilt-dev/tilt-example-builders.

Kubectl Build

If you’re perfectly happy with your Dockerfile, but just want to move Docker builds off your laptop, kubectl build is for you.

It’s a drop-in replacement for docker build.

When you run kubectl build, the CLI:

  • Deploys a Docker build server to your Kubernetes cluster.

  • Sends your Dockerfile and source code to the build server.

  • Runs the build and pushes to a registry.

A few months ago, Gaëtan Lehmann added an extension for Tilt that lets you use kubectl build for image-building.

If you have this Tiltfile:

docker_build(
  'example-python-image', 
  '.', 
  build_args={'flask_env': 'development'},
  live_update=[
    sync('.', '/app'),
  ])

You can replace it with:

load('ext://kubectl_build', 'kubectl_build')
kubectl_build(
  'example-python-image', 
  '.', 
  build_args={'flask_env': 'development'},
  live_update=[
    sync('.', '/app'),
  ])

The main difference is that you will now see a buildkit server in your cluster:

$ kubectl get deployment
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
buildkit         1/1     1            1           70m
example-python   1/1     1            1           7s

This can be nice if you have a powerful cluster and a 13” laptop!

Read the full docs on the kubectl_build extension here.

Ko

Maybe you’re not happy with your Dockerfile.

The Dockerfile view of the world is: to build an image, run some commands in a container, then save the results of those commands.

The Ko view is: an image is a tarball. If you have a Mac, why do you need a Linux VM to run a container if you want a tarball? You already have good local tools for making tarballs.

Ko is an image builder aimed at Go apps. Go apps are particularly ill-suited for docker build. The Go toolchain is well-optimized to run locally and builds a single binary. Ko builds your Go app locally, then packs it into an image. No build containers or VMs needed!

A while back, I wrote a small extension that lets you use ko in Tilt. Here’s what it looks like:

load('ext://ko', 'ko_build')

ko_build(
  'example-go-image',
  './',
  deps=['.']
)

The upside of ko is that it’s super simple to get started.

The downside is that if you want any features that can’t be expressed in a static Go binary, you’ll need to configure ko to include them. This includes: extra files in the container filesystem, dynamic C libraries, or hot-reload. For example, this example project uses the kodata directory and KO_DATA_PATH env variable to load HTML templates.

Read the full docs on the ko extension here.

Pack

pack is also addressing people who are not happy maintaining Dockerfiles.

But where Ko’s philosophy states that you don’t need a container to build an image, Buildpack takes the viewpoint that you need MORE containers.

Joking! pack’s real philsophy: you shouldn’t have to worry about low-level details of how images are assembled. You should have bigger building blocks and let your builder decide how to quickly combine them.

With pack, there’s a stronger separation between the container you use to build an image and the container you end up running. The container images you use to build stuff are called buildpacks.

Separating these out has some big advantages!

  • You can stack buildpacks to mix in the features you want (like hot-reload tools).

  • The buildpack keeps out lots of junk you don’t want in the final image (like compilers).

The pack extension was one of the first Tilt extensions! Gareth Rushgrove added it when we were still experimenting with the extension system.

Since then, we worked closely with Daniel Mikusa of the buildpacks team to make it even better.

Here’s what it looks like in a Tiltfile:

pack(
  'example-python-image', 
  '.',
  buildpacks=[
    'gcr.io/paketo-buildpacks/python',
    'gcr.io/paketo-buildpacks/environment-variables',
  ],
  env_vars=['BPE_FLASK_ENV=development'],
  live_update=[
    sync('.', '/workspace'),
  ])

In this example, we’re using two buildpacks: a python buildpack that sets up a Python image, and a environment-variables buildpack that sets FLASK_ENV=development to get live-reloading (Flask is the python framework that does the live-reload.)

Read the full docs on the pack extension here.

Fun aside: Future Buildpack Work

The layering approach to adding new functionality is getting a lot of excitement right now from container nerds.

For security nerds, you can better track where source files come from and attach SBOMs (which are way beyond the scope of this post, but are fun to say out load.)

One of the big reasons Tilters are excited about buildpacks is that there’s a lot of configuration in your Tiltfile that would be simpler if it was in the buildpack. Daniel has been working on an RFC to add live-reload behavior to each buildpack.

More medium-term, we believe that buildpacks should be able to auto-detect live-update sync rules that Tilt can read, so you don’t need to keep them up-to-date in your Tiltfile.

Testing Out a New Image Builder

I hope this guide gave you a good tour of different image builders and how to use them with Tilt. You can see examples of all these image builders in the tilt-example-builders repo.

There are more image builders in the world that I didn’t cover! I like this post Jérôme Petazzoni wrote last year about a few more, including the image builders in Nix and Bazel:

https://jpetazzo.github.io/2020/04/01/quest-minimal-docker-images-part-3/

If you want to try out a new image builder today, you don’t have to switch at once. The Tiltfile config API lets you add custom flags to tilt up, so that different users on your team can try out different image builders.

docker build is a good general-purpose builder, but depending on what you’re working on, there may be a faster and easier image builder out there for you!

Related

Keep up with Developments in Multi-Service Development
 
 
 
Keep up with Developments in Multi-Service Development

Already have a Dockerfile and a Kubernetes config?

You’ll be able to setup Tilt in no time and start getting things done. Check out the docs! 

Having trouble developing your servers in Kubernetes?