How do you decide which buildx driver and exporter every team's image builds should use, from laptops to CI?
answer
- Start from where the artefact lands
- Different answer per environment
- Insulate CI from the host engine
- Shared builder is shared infrastructure
- Enforce by template, not by README
basics
~20 sDecide by where the result must land and how insulated the build must be from the host: the built-in docker driver for laptop iteration, and a docker-container or remote builder in CI with push as the default exporter.
solid answer
~50 sStart from outputs, not from driver names. On laptops the built-in `docker` driver is right because building and then `docker run` must just work; a container builder there mostly generates "where did my image go" tickets. In CI the plain engine builder makes the result a function of the runner's engine version, so a `docker-container` builder — pinned and configured deliberately — is the better default, with `--push` as the exporter and downstream jobs consuming by tag or digest rather than a local image store. A `remote` builder is worth it when ephemeral runners keep discarding builder state, but it is shared infrastructure: multi-tenant isolation, capacity, a failure domain where nobody ships when it is down, and an owner for its upgrades. Govern the `# syntax=` frontend pin the same way, and enforce all of it through a shared CI template rather than a README.
go deeper
Know that builds do not all run in the same place: your laptop's Docker Engine has a builder built in, and CI may use a separate one whose results have to be exported explicitly.
Be able to compare the docker, docker-container and remote buildx drivers on concrete grounds — where BuildKit runs, what is available by default, and what an image build produces in each case.
Show you can pick per environment and defend it: why CI should not depend on the runner's engine version, why publishing by digest beats assuming a local image store, and what you would monitor.
Own the whole picture — isolation and tenancy of a shared builder, its failure domain and owner, the frontend-pin policy across the fleet, and the mechanism that keeps the standard from drifting.
### The decision is about where results must land Builder choice looks like a tooling preference and is really a question about outputs. A buildx **driver** decides where BuildKit itself runs; the **exporter** decides where the finished result goes. Standardising them means answering, per environment: what is the artefact, and who consumes it next? Three environments, three honest answers. **The developer loop.** Someone iterating on one service wants `docker build` followed by `docker run` to work, with no ceremony. The built-in `docker` driver gives exactly that, because BuildKit is inside the engine and the result lands in its image store. Prescribing a container builder here is a tax with no return, and it produces the classic support ticket: build succeeded, `docker images` empty. The exception is a developer running a 23-service local stack, where builds are the bottleneck; a single shared container builder can then be worth the extra flag, but it should be opt-in tooling, not a rule. **CI.** Here the plain engine builder is the weak choice, because the build becomes a function of whatever engine version the runner image happens to ship. A `docker-container` builder pins BuildKit independently of the host engine and can be configured deliberately, which is what you want when the same Dockerfile must produce the same result on every runner. The exporter should be `--push` by default: CI's product is a published artefact, and downstream jobs should consume it by tag or, better, by digest, rather than relying on a local image store that a fresh runner will not have. Add `--load` only for a job that genuinely tests locally before publishing. **Shared build infrastructure.** A `remote` driver — buildx talking to a `buildkitd` that someone else runs — is the answer when ephemeral runners keep throwing away builder state, or when builds need machines the runners are not. It is also the option that carries the most organisational weight, and it deserves an explicit decision rather than drifting into existence. ### What a shared builder actually costs Concentrating builds on one BuildKit is a real architectural change, and the tradeoffs are the substance of a principal-level answer. *Isolation.* Every team's build context, build arguments and in-flight credentials pass through one process. A shared builder is a multi-tenant system: it needs access control on who may target it, and a clear answer to whether team A can influence team B's builds. *Failure domain.* When it is down, nobody ships. That argues for capacity headroom, a documented fallback to a local builder, and monitoring owned by whoever runs the platform — not by the first team that notices. *Noisy neighbours.* Concurrent builds share CPU, memory and disk on one host. Someone must own quota and eviction policy, and be able to say why a build queued for six minutes. *Lifecycle.* Builders drift. Who upgrades BuildKit, on what cadence, and how is a regression rolled back? If the answer is "nobody", the shared builder becomes the oldest, least understood component in the pipeline. ### Governing the Dockerfile frontend The same standardisation question applies to the `# syntax=docker/dockerfile:1` directive, and it is easy to forget because it lives in the Dockerfile rather than in the pipeline. Two defensible policies: float on the `:1` major tag so every repository picks up syntax and fixes automatically, accepting that the parser can change between two builds of the same commit; or mirror the frontend into an internal registry and pin it, buying reproducibility and supply-chain control at the price of someone owning the bumps. What is not defensible is having both by accident, so that some repositories build with a frontend from last year and others with today's. ### Making it stick Conventions written in a README drift within a quarter. What holds is mechanism: a shared CI template or a thin build wrapper that selects the builder, sets the exporter and asserts the frontend pin, so the default path is the standard path and deviation is visible in a diff. Pair it with a small set of signals — build duration and queue time per service, failure rate by builder, how many repositories deviate from the template — because "which builder is right" is a question you should be able to re-answer with data six months later. ### How to answer this in an interview Do not lead with a driver name. Lead with the axes — where the artefact must land, how much the build must be insulated from the host, who owns the builder's lifecycle, and what happens when it is unavailable — then place the drivers on those axes, and be explicit about the one that costs the most: a shared builder is shared infrastructure, with the isolation, capacity and ownership obligations that phrase implies.
- Developers building a 23-service local stack complain builds dominate their day. Does that change your recommendation?It can. That is the one developer case where a shared builder earns its keep, because the work is large enough that host-engine limits and per-laptop capacity start to matter. I would make it opt-in tooling with an obvious fallback to the local `docker` driver, and I would measure it first: if the time is really going to context transfer or to rebuilding everything on every change, a bigger builder just moves the same waste somewhere more expensive.
- What would make you reverse a decision to run a shared remote builder?Two signals. First, availability: if build outages become a recurring source of shipping delay and nobody owns the on-call for it, the concentration is not paying for itself. Second, tenancy pressure — teams needing different BuildKit configurations, or a credible objection to their build inputs passing through infrastructure another team can reach. Either is a reason to fall back to per-runner container builders and accept the duplication.
- How do you keep developer builds and CI builds from diverging in ways that only surface at release time?Make the invocation the same artefact in both places: one wrapper or CI template that picks the builder, sets the exporter and asserts the frontend pin, so a developer runs literally what CI runs with a different exporter. Then treat deviation as visible — count repositories not using the template — and keep the frontend policy uniform, because two Dockerfile dialects across a fleet is exactly the divergence that appears late.
saying these in an interview costs you the question
- Names one driver as universally correct
- Puts a container builder on every laptop by default
- Treats a shared builder as free capacity
- Ignores who owns the builder's upgrades and outages
- Leaves the frontend pin to each repository's habit
- Writes the standard in a README and calls it enforced