skip to content

Why does a CI job run `docker buildx create --driver docker-container` before it builds?

level: middleimportance: must knowfreq 63%

answer

  1. The command registers something, installs nothing
  2. Ask which BuildKit does the work
  3. The embedded builder has hard limits
  4. Cache export and platforms are the drivers
  5. Container plus state volume, gone with the runner

basics

~20 s

It creates a builder instance backed by a BuildKit container rather than the BuildKit embedded in the daemon. That driver is what unlocks exporting cache to an external backend, building for other platforms, and a custom BuildKit config.

solid answer

~50 s

`docker buildx create` does not install anything; it registers a **builder instance** and picks the **buildx driver** that backs it. The default builder uses the `docker` driver, which is the BuildKit embedded in dockerd: convenient, because results load straight into the local image store, but limited — it can only export cache inline in the image, it cannot build for other platforms, and it cannot take a custom BuildKit config. The `docker-container` driver instead starts a dedicated BuildKit container on the same engine, which supports full cache export to an external backend, multi-platform builds, and a `buildkitd` config file supplied with `--config`. A CI job creates one because those capabilities are precisely what CI needs. The cost is that this builder has its own content store, so the result must be exported explicitly, and on an ephemeral runner both the container and its state volume disappear with the machine.

code

bash · 5 lines
bash
docker buildx create --name ci --driver docker-container --use --bootstrap
docker buildx ls
docker buildx inspect ci
# ... build ...
docker buildx rm ci

go deeper

for a junior

Recall that a builder instance is a named pointer to a BuildKit, that create picks its driver, and that --use makes it the one your builds go to. Knowing the command's shape is enough at this level.

for a middle

Explain the capability gap concretely: the embedded docker driver exports cache only inline, builds for the host platform, and takes no BuildKit config, while the container driver lifts all three. Describe the builder's container plus state volume.

for a senior

Show that you reason about lifecycle and failure modes — bootstrapping eagerly so setup failures are attributed correctly, checking buildx ls when a job behaves like the default, and knowing the builder's state dies with an ephemeral machine.

for a principal

Own the fleet-wide decision: a per-job container builder, a long-lived remote one, or a mix — and the operating cost that follows, including who configures BuildKit, who watches its disk, and what a builder outage does to every pipeline at once.

### Builders, drivers and what `create` actually creates Buildx separates *the CLI you type* from *the BuildKit that does the work*. A **builder instance** is a named entry that points the CLI at one BuildKit; `docker buildx ls` lists them and marks the current one. `docker buildx create` adds an entry and chooses the **driver** — the mechanism that provides the BuildKit behind it. It installs no software and provisions no machine. The drivers you meet in practice: - **`docker`** — the BuildKit embedded in the Docker daemon. This backs the builder called `default` and is what plain `docker build` uses. You cannot create one; it is simply there. - **`docker-container`** — buildx starts a BuildKit container on the target engine (named after the builder, with a Docker volume holding its state) and talks to it. - **`remote`** — connect to a BuildKit daemon that someone else runs, over an address. ### Why the default driver is not enough for CI The `docker` driver is deliberately conservative because it lives inside the daemon. Its limits are the reason CI jobs move off it: 1. **Cache export.** The embedded builder can only write cache *inline* — folded into the image it just published. It cannot export a standalone cache to an external backend that a different machine can import later. On throwaway runners, an external cache is the only cache there is, so this limit alone forces the switch. 2. **Platforms.** The embedded builder builds for the host's architecture. Producing an image for a different one, or a manifest list covering several, needs the container driver. 3. **BuildKit configuration.** `docker buildx create --config buildkitd.toml` hands the builder its own configuration — registry settings, garbage-collection policy — without touching the host daemon's `daemon.json` or restarting it. That is a real advantage on a shared runner you do not own. 4. **Isolation.** The builder is a container with its own state volume. You can inspect it, prune it, or delete it (`docker buildx rm`) without disturbing the engine's images and containers. ### The lifecycle on an ephemeral runner The conventional invocation is `docker buildx create --name ci --driver docker-container --use --bootstrap`: - `--name` gives the instance a stable name so later commands can target it with `--builder ci`. - `--use` makes it the current builder, so subsequent `docker buildx build` calls go to it. Forgetting `--use` is the classic mistake: the build silently runs on `default` and then fails when it tries to export cache to an external backend. - `--bootstrap` starts the BuildKit container immediately instead of lazily on the first build, so container startup and image pull failures surface as a create failure rather than as a mysterious slow first build. `docker buildx inspect ci` shows the driver, the BuildKit version and the platforms the builder advertises — a useful one-line check in a job log when something is unexpectedly building for the wrong architecture. On an ephemeral runner none of this survives. The BuildKit container and its state volume live on a machine that is destroyed when the job ends, so every job creates its builder from scratch. That is a few seconds of setup — pulling the BuildKit image and starting it — and it is the reason the cache has to live somewhere the runner is not. Teams sometimes reach for the `remote` driver precisely to escape that, pointing every job at a long-lived BuildKit elsewhere. ### What changes for the rest of the job Two behaviours differ once the job is on a container-driver builder, and both surprise people: - **Results are not loaded automatically.** The builder has its own content store, so the job must export deliberately — publish with `--push`, or import into the engine with `--load` if something local needs to run it. - **The build context is transferred to the builder.** Files are streamed to BuildKit rather than read in-process by the daemon, so an oversized context is more visible: a fat directory shows up as transfer time in the log before any instruction runs. ### Sanity checks when it misbehaves If a job behaves as though nothing changed, check `docker buildx ls` for which builder is actually current (`--use` missing, or a `--builder` flag pointing elsewhere), then `docker buildx inspect --bootstrap` to confirm the BuildKit container really started. If the BuildKit image cannot be pulled — a locked-down runner with no route to fetch it — `create --bootstrap` fails there rather than five minutes into the build, which is the whole point of bootstrapping eagerly.

  • What does `--bootstrap` change, and why would a CI job bother?
    Without it, the BuildKit container is started lazily on the first build; with it, `create` starts it right away. On a runner that means image-pull and startup failures appear as a fast, clearly-attributed setup failure instead of being folded into the first build's timing and log. It also keeps the build step's duration honest, which matters when you are measuring cold versus warm builds.
  • When would you point CI at the `remote` driver instead of creating a container builder per job?
    When you want a BuildKit that outlives the runner — its local cache stays warm, so jobs skip both the builder startup and the cost of importing cache over the network. You trade that for a stateful service someone must run, size and garbage-collect, and for a shared cache and shared cache mounts across everything that uses it.
  • A job creates the builder but the build still behaves like the default one. What do you check?
    Whether the builder is actually selected. `docker buildx ls` shows which instance is current; a missing `--use`, a `--builder` flag pointing elsewhere, or a fresh shell in a later job step that lost the selection all produce a build that quietly runs on `default`. `docker buildx inspect` on the intended builder confirms its driver and that BuildKit is up.

saying these in an interview costs you the question

  • Thinks buildx create installs Docker or BuildKit on the runner
  • Believes the default builder can export cache to an external backend
  • Omits --use and cannot explain why nothing changed
  • Expects the builder container to survive an ephemeral runner
  • Confuses the buildx driver with a storage or network driver
  • Says the driver choice only matters for multi-platform builds

context