Repository: docker/compose
Pull request #12485 (2025-01-22 13:42:28)

support additional_context reference to another service
- classic builder doesn't support additional context
- buildkit builder detects `service:` prefix in contexts and builds images in order (in addition to runtime dependencies, to preserve backward compatibility)
- bake builder delegates to `buildx bake` with a named context for service's target

```
$ COMPOSE_BAKE=false docker compose build
INFO[0000] additional_context with "service:"" is better supported when delegating build go bake. Set COMPOSE_BAKE=true 
[+] Building 2.0s (16/16) FINISHED                                                                                                       docker:desktop-linux
 => [a internal] load build definition from Dockerfile.a                                                                                                 0.0s
...
 => => naming to docker.io/library/b:latest                                                                                                              0.0s
 => => unpacking to docker.io/library/b:latest                                                                                                           0.0s
 => [b] resolving provenance for metadata file                                                                                                           0.0s
[+] Building 2/2
 ✔ a  Built                                                                                                                                              0.0s 
 ✔ b  Built  
```


Patch:
diff --git a/cmd/compose/build.go b/cmd/compose/build.go
index 4b0e59bfd..ca9e0f259 100644
--- a/cmd/compose/build.go
+++ b/cmd/compose/build.go
@@ -68,17 +68,16 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
 		uiMode = "rawjson"
 	}
 	return api.BuildOptions{
-		Pull:          opts.pull,
-		Push:          opts.push,
-		Progress:      uiMode,
-		Args:          types.NewMappingWithEquals(opts.args),
-		NoCache:       opts.noCache,
-		Quiet:         opts.quiet,
-		Services:      services,
-		Deps:          opts.deps,
-		SSHs:          SSHKeys,
-		Builder:       builderName,
-		Compatibility: opts.Compatibility,
+		Pull:     opts.pull,
+		Push:     opts.push,
+		Progress: uiMode,
+		Args:     types.NewMappingWithEquals(opts.args),
+		NoCache:  opts.noCache,
+		Quiet:    opts.quiet,
+		Services: services,
+		Deps:     opts.deps,
+		SSHs:     SSHKeys,
+		Builder:  builderName,
 	}, nil
 }
 
diff --git a/go.mod b/go.mod
index 654daf7be..b288db103 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/Microsoft/go-winio v0.6.2
 	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
 	github.com/buger/goterm v1.0.4
-	github.com/compose-spec/compose-go/v2 v2.4.7
+	github.com/compose-spec/compose-go/v2 v2.4.8-0.20250130174723-77ab539e4f3f
 	github.com/containerd/containerd/v2 v2.0.2
 	github.com/containerd/platforms v1.0.0-rc.1
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
diff --git a/go.sum b/go.sum
index b701cc843..977494a32 100644
--- a/go.sum
+++ b/go.sum
@@ -81,8 +81,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
 github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
-github.com/compose-spec/compose-go/v2 v2.4.7 h1:WNpz5bIbKG+G+w9pfu72B1ZXr+Og9jez8TMEo8ecXPk=
-github.com/compose-spec/compose-go/v2 v2.4.7/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
+github.com/compose-spec/compose-go/v2 v2.4.8-0.20250130174723-77ab539e4f3f h1:turCjSVHj+0P+G6kuRsJfhhYzp1ULfTv7GVzv1dgIHQ=
+github.com/compose-spec/compose-go/v2 v2.4.8-0.20250130174723-77ab539e4f3f/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
 github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
 github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=
 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 2495665a0..e73bfcc9e 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -155,8 +155,6 @@ type BuildOptions struct {
 	Memory int64
 	// Builder name passed in the command line
 	Builder string
-	// Compatibility let compose run with best backward compatibility
-	Compatibility bool
 }
 
 // Apply mutates project according to build options
diff --git a/pkg/compose/build.go b/pkg/compose/build.go
index af38c05ed..3e842abf7 100644
--- a/pkg/compose/build.go
+++ b/pkg/compose/build.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/containerd/platforms"
@@ -71,7 +72,33 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 	if options.Deps {
 		policy = types.IncludeDependencies
 	}
-	err := project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
+
+	serviceDeps := false
+	project, err := project.WithServicesTransform(func(serviceName string, service types.ServiceConfig) (types.ServiceConfig, error) {
+		if service.Build != nil {
+			for _, c := range service.Build.AdditionalContexts {
+				if t, found := strings.CutPrefix(c, types.ServicePrefix); found {
+					serviceDeps = true
+					if service.DependsOn == nil {
+						service.DependsOn = map[string]types.ServiceDependency{}
+					}
+					service.DependsOn[t] = types.ServiceDependency{
+						Condition: "build", // non-canonical, but will force dependency graph ordering
+					}
+				}
+			}
+		}
+		return service, nil
+	})
+	if err != nil {
+		return imageIDs, err
+	}
+
+	if serviceDeps {
+		logrus.Infof(`additional_context with "service:"" is better supported when delegating build go bake. Set COMPOSE_BAKE=true`)
+	}
+
+	err = project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
 		if service.Build == nil {
 			return nil
 		}
@@ -536,6 +563,11 @@ func getImageBuildLabels(project *types.Project, service types.ServiceConfig) ty
 func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedContext {
 	namedContexts := map[string]build.NamedContext{}
 	for name, contextPath := range additionalContexts {
+		if _, found := strings.CutPrefix(contextPath, types.ServicePrefix); found {
+			// image we depend on has been build previously, as we run in dependency order.
+			// this assumes use of docker engine builder, so that build can access local images
+			continue
+		}
 		namedContexts[name] = build.NamedContext{Path: contextPath}
 	}
 	return namedContexts
diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go
index 2a4c2be5d..5c7fabc4e 100644
--- a/pkg/compose/build_bake.go
+++ b/pkg/compose/build_bake.go
@@ -105,14 +105,17 @@ type bakeTarget struct {
 	Tags             []string          `json:"tags,omitempty"`
 	CacheFrom        []string          `json:"cache-from,omitempty"`
 	CacheTo          []string          `json:"cache-to,omitempty"`
+	Target           string            `json:"target,omitempty"`
 	Secrets          []string          `json:"secret,omitempty"`
 	SSH              []string          `json:"ssh,omitempty"`
 	Platforms        []string          `json:"platforms,omitempty"`
-	Target           string            `json:"target,omitempty"`
 	Pull             bool              `json:"pull,omitempty"`
 	NoCache          bool              `json:"no-cache,omitempty"`
+	NetworkMode      string            `json:"network,omitempty"`
+	NoCacheFilter    []string          `json:"no-cache-filter,omitempty"`
 	ShmSize          types.UnitBytes   `json:"shm-size,omitempty"`
 	Ulimits          []string          `json:"ulimits,omitempty"`
+	Call             string            `json:"call,omitempty"`
 	Entitlements     []string          `json:"entitlements,omitempty"`
 	Outputs          []string          `json:"output,omitempty"`
 }
@@ -124,11 +127,6 @@ type buildStatus struct {
 }
 
 func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
-	cw := progress.ContextWriter(ctx)
-	for name := range serviceToBeBuild {
-		cw.Event(progress.BuildingEvent(name))
-	}
-
 	eg := errgroup.Group{}
 	ch := make(chan *client.SolveStatus)
 	display, err := progressui.NewDisplay(os.Stdout, progressui.DisplayMode(options.Progress))
@@ -191,7 +189,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
 
 		cfg.Targets[serviceName] = bakeTarget{
 			Context:          build.Context,
-			Contexts:         additionalContexts(build.AdditionalContexts, service.DependsOn, options.Compatibility),
+			Contexts:         additionalContexts(build.AdditionalContexts),
 			Dockerfile:       dockerFilePath(build.Context, build.Dockerfile),
 			DockerfileInline: build.DockerfileInline,
 			Args:             args,
@@ -221,7 +219,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
 		return nil, err
 	}
 
-	logrus.Debugf("bake config:\n%s", string(b))
+	logrus.Debugf("bake build config:\n%s", string(b))
 
 	metadata, err := os.CreateTemp(os.TempDir(), "compose")
 	if err != nil {
@@ -320,6 +318,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
 		return nil, err
 	}
 
+	cw := progress.ContextWriter(ctx)
 	results := map[string]string{}
 	for name, m := range md {
 		results[name] = m.Digest
@@ -328,14 +327,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
 	return results, nil
 }
 
-func additionalContexts(contexts types.Mapping, dependencies types.DependsOnConfig, compatibility bool) map[string]string {
+func additionalContexts(contexts types.Mapping) map[string]string {
 	ac := map[string]string{}
-	if compatibility {
-		for name := range dependencies {
-			ac[name] = "target:" + name
-		}
-	}
 	for k, v := range contexts {
+		if target, found := strings.CutPrefix(v, types.ServicePrefix); found {
+			v = "target:" + target
+		}
 		ac[k] = v
 	}
 	return ac


Test patch:
diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go
index 3459a8616..9231ab1ec 100644
--- a/pkg/e2e/build_test.go
+++ b/pkg/e2e/build_test.go
@@ -271,18 +271,34 @@ func TestBuildImageDependencies(t *testing.T) {
 	t.Run("ClassicBuilder", func(t *testing.T) {
 		cli := NewCLI(t, WithEnv(
 			"DOCKER_BUILDKIT=0",
-			"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
+			"COMPOSE_FILE=./fixtures/build-dependencies/classic.yaml",
+		))
+		doTest(t, cli)
+	})
+
+	t.Run("BuildKit by dependency order", func(t *testing.T) {
+		cli := NewCLI(t, WithEnv(
+			"DOCKER_BUILDKIT=1",
+			"COMPOSE_FILE=./fixtures/build-dependencies/classic.yaml",
 		))
 		doTest(t, cli)
 	})
 
-	t.Run("BuildKit", func(t *testing.T) {
+	t.Run("BuildKit by additional contexts", func(t *testing.T) {
 		cli := NewCLI(t, WithEnv(
 			"DOCKER_BUILDKIT=1",
 			"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
 		))
 		doTest(t, cli)
 	})
+
+	t.Run("Bake by additional contexts", func(t *testing.T) {
+		cli := NewCLI(t, WithEnv(
+			"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=1",
+			"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
+		))
+		doTest(t, cli)
+	})
 }
 
 func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
diff --git a/pkg/e2e/fixtures/build-dependencies/classic.yaml b/pkg/e2e/fixtures/build-dependencies/classic.yaml
new file mode 100644
index 000000000..b0dbbaad0
--- /dev/null
+++ b/pkg/e2e/fixtures/build-dependencies/classic.yaml
@@ -0,0 +1,14 @@
+services:
+  base:
+    image: base
+    init: true
+    build:
+      context: .
+      dockerfile: base.dockerfile
+  service:
+    init: true
+    depends_on:
+      - base
+    build:
+      context: .
+      dockerfile: service.dockerfile
diff --git a/pkg/e2e/fixtures/build-dependencies/compose.yaml b/pkg/e2e/fixtures/build-dependencies/compose.yaml
index b0dbbaad0..1c914b2b4 100644
--- a/pkg/e2e/fixtures/build-dependencies/compose.yaml
+++ b/pkg/e2e/fixtures/build-dependencies/compose.yaml
@@ -7,8 +7,8 @@ services:
       dockerfile: base.dockerfile
   service:
     init: true
-    depends_on:
-      - base
     build:
       context: .
+      additional_contexts:
+        base: "service:base"
       dockerfile: service.dockerfile