Bake’s parallel target execution is less about spinning up more processes and more about intelligently orchestrating a dependency graph that happens to have multiple independent branches.

Let’s see it in action. Imagine you’re building a web application with a backend API and a frontend.

{
  "targets": {
    "backend": {
      "type": "docker",
      "dockerfile": "Dockerfile.backend",
      "dependencies": ["common-libs"],
      "context": "./backend"
    },
    "frontend": {
      "type": "docker",
      "dockerfile": "Dockerfile.frontend",
      "dependencies": ["common-libs"],
      "context": "./frontend"
    },
    "common-libs": {
      "type": "script",
      "script": "scripts/build-common-libs.sh"
    }
  }
}

When you run bake build, Bake first identifies that common-libs has no dependencies. It starts executing scripts/build-common-libs.sh. Simultaneously, it sees that both backend and frontend depend on common-libs. Once common-libs finishes, Bake can then start building backend and frontend in parallel because their only remaining dependency (common-libs) is met. It doesn’t need to wait for backend to finish before starting frontend, or vice-versa.

The mental model here is a Directed Acyclic Graph (DAG). Bake builds this DAG from your bake.json file. Nodes are your targets, and directed edges represent dependencies. Bake’s scheduler then performs a topological sort on this graph. Any node that has all its incoming edges (dependencies) satisfied can be executed. If multiple nodes become ready simultaneously, Bake can execute them in parallel, provided your system has the resources.

The key levers you control are the dependencies field within each target definition. A dependency means "this target cannot start until that target has successfully completed." By carefully defining these relationships, you tell Bake how your build can be broken down into independent units.

You might think that Bake magically detects parallelism. It doesn’t. It’s purely driven by the explicit dependency graph you provide. If you define a depends on b, and b depends on c, a will never run in parallel with b, regardless of how many CPU cores you have. The parallelism emerges when you have multiple independent branches of execution, like in our backend/frontend example where both depended only on a single common artifact.

The real trick to maximizing parallel execution isn’t about telling Bake to run things in parallel; it’s about minimizing dependencies between targets that could be run in parallel. For instance, if your common-libs were actually two separate libraries, common-libs-a and common-libs-b, and both backend and frontend depended on both of them, they’d still have to wait for both to complete. But if backend only needed common-libs-a and frontend only needed common-libs-b, then common-libs-a and common-libs-b could run in parallel, and then backend and frontend could also run in parallel, all because the graph branches early.

The next concept you’ll grapple with is managing build artifacts and ensuring that parallel builds don’t overwrite each other’s outputs.

Want structured learning?

Take the full Buildkit course →