Back to blog
DockerDevOpsLaravel

Buildx 0.38.0 brings order to Laravel builds with Bake

Docker Buildx adds Bake controls for concurrency and failures, making Laravel image publishing more deliberate.

By Isma5 min read

Four images should not behave like four unrelated scripts

A self-hosted Laravel application often ends up with separate images for PHP-FPM, queue workers, the scheduler, and the frontend. That separation is sensible because each process has different dependencies, deployment cycles, and runtime requirements. The trouble starts when building and publishing them relies on several disconnected commands, uncontrolled concurrency, and unclear results after a failure.

Bake lets me describe those targets in one file and run them as a set. Buildx 0.38.0 adds --jobs, with -j as its short form, so I can decide how many Bake targets run at once and what happens when one of them breaks. (github.com)

One Bake definition for the application and its processes

I would start with a docker-bake.hcl file that maps directly to the artifacts I deploy. PHP-FPM, workers, and the scheduler do not need to duplicate their full configuration when they share a Dockerfile: they can use the same context, tags, and build inputs while selecting different Dockerfile targets. The frontend may need its own recipe, but it still belongs to the same release group.

group "app" {
  targets = ["php", "worker", "scheduler", "frontend"]
}
 
target "php" {
  context    = "."
  dockerfile = "Dockerfile"
  target     = "php-fpm"
  tags       = ["registry.example.com/mi-laravel/php:latest"]
  output     = ["type=registry"]
}
 
target "worker" {
  context    = "."
  dockerfile = "Dockerfile"
  target     = "worker"
  tags       = ["registry.example.com/mi-laravel/worker:latest"]
  output     = ["type=registry"]
}
 
target "scheduler" {
  context    = "."
  dockerfile = "Dockerfile"
  target     = "scheduler"
  tags       = ["registry.example.com/mi-laravel/scheduler:latest"]
  output     = ["type=registry"]
}
 
target "frontend" {
  context    = "."
  dockerfile = "Dockerfile.frontend"
  tags       = ["registry.example.com/mi-laravel/frontend:latest"]
  output     = ["type=registry"]
}

The benefit is not merely a tidier file. The app group states that these four images are part of one deployment, even if their builds are independent internally. That gives me one reproducible command for local checks and CI instead of failure handling scattered through shell scripts.

Putting a ceiling on builder pressure

The simplest form of the new option is a number. -j=2 schedules no more than two Bake targets at a time, and it is equivalent to --jobs=parallel=2. It limits concurrency between targets, not BuildKit's own internal parallelism while processing the steps inside each target. (docs.docker.com)

docker buildx bake app -j=2

That is useful on a homelab or a modest CI runner where all four images compete for CPU, memory, layer cache, or registry access. It does not make the machine faster, but it can stop a frontend build and several PHP images from overwhelming the builder at the same time. The right value depends on the hardware and the Dockerfiles, so it should come from observing the actual workload rather than copying a random setting.

Deciding what a failed target means

The default mode is fail-fast. When one target fails, Bake stops the overall build and cancels targets that are still running, but outputs that successful targets have already written are not removed. It is a sensible way to surface an error quickly, though it does not guarantee that a failed run leaves the registry untouched. (docs.docker.com)

defer-error takes a different approach for independent targets: it lets them continue after another target has failed, then returns an error when all possible work is done. I would use it in CI when I want a fuller picture of failures or when successful, unrelated builds are still useful. Since successful targets can export their outputs, it is not the right mode for a strictly coordinated publication step.

docker buildx bake app --jobs=defer-error,parallel=2

For publishing the four images as one logical release, defer-output is the useful mode. Bake waits until every participating target has successfully evaluated its build result before any target starts exporting output. If the worker image fails to build, PHP-FPM, scheduler, and frontend exports are held back. (docs.docker.com)

docker buildx bake app --jobs=defer-output

The important limitations of defer-output

defer-output is not a registry transaction and it does not provide rollback. If an export fails after other exports have completed, Buildx does not undo the successful ones, so partial output can still exist. There is also a boundary in multi-node builds: synchronization covers each target's solve and export stages, not every later operation Buildx may perform when merging manifests or pushing to a registry. (docs.docker.com)

It is also not compatible with every concurrency limit. With defer-output, all participating targets need to reach the output boundary together, so Bake rejects a non-zero jobs limit that is smaller than the number of participating targets. Linked targets and implicit targets referenced through target: contexts can trigger the same restriction. (docs.docker.com)

This does not replace immutable tags, tests, image scanning, or promotion between environments. For a serious deployment, I would still tag every image with the commit or release version and treat latest as an optional convenience tag. What --jobs adds is an explicit build and export policy, not a complete release-management system.

A small option with a clearer pipeline contract

For a Laravel application with related images, my default would be a Bake group, consistent tags, and --jobs=defer-output in the publishing step. If the builder needs stricter resource protection, I would first revisit the target layout or CI environment because a jobs limit below the number of targets does not fit that mode. For diagnostic runs where collecting all failures matters more, I would deliberately switch to defer-error.

This will not fix a poorly designed Dockerfile. It does stop concurrency and failure behavior from being accidental defaults or hidden shell-script details. On self-hosted projects, where a private registry and builder often share limited resources, making that choice visible in the Bake command is a practical improvement.


Source: Buildx v0.38.0 · Official documentation: docker buildx bake (github.com)

Newsletter

What I learn coding with AI, every two weeks in your inbox.

Real numbers, mistakes included. No spam, unsubscribe in one click.

No spam, unsubscribe in one click. I only use your email to send you this and the newsletter. More in the privacy policy.