The old builder executes every instruction in order on one goroutine. BuildKit builds a graph, runs independent branches concurrently, and skips stages nothing depends on.
$ DOCKER_BUILDKIT=1 docker build -t app .
[+] Building 18.4s (14/14) FINISHED
=> [internal] load build definition 0.0s
=> [builder 3/5] RUN composer install --no-dev 11.2s
=> [assets 3/4] RUN npm ci 9.8s ← concurrent
=> [stage-2 4/6] COPY --from=assets /app/public/build ... 0.3s
# same Dockerfile, old builder: 31.6s — the two stages ran in series
The concurrency is free on any multi-stage build whose stages do not depend on each other, which is most of them — a PHP stage and an asset stage typically share only the final COPY. Setting the environment variable per invocation is the 18.09 way; in 19.03 it can go in /etc/docker/daemon.json as {"features": {"buildkit": true}} so nobody has to remember. The output format is different enough that a CI log parser may need adjusting, which is the only migration cost.