Repository: docker/compose
Pull request #11465 (2024-02-05 23:25:20)

feat(tracing): add project hash attribute
**What I did**
Hash the project config and add it as an attribute. This can be used to group multiple spans.

**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
![gecko](https://github.com/docker/compose/assets/841263/2b00a72f-f4dd-4cd3-8e64-b506fd615801)



Patch:
diff --git a/cmd/compose/attach.go b/cmd/compose/attach.go
index 1899edef1..340079cf6 100644
--- a/cmd/compose/attach.go
+++ b/cmd/compose/attach.go
@@ -64,7 +64,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
 }
 
 func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
-	projectName, err := opts.toProjectName(dockerCli)
+	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/build.go b/cmd/compose/build.go
index 5f855c922..e96fbfb1c 100644
--- a/cmd/compose/build.go
+++ b/cmd/compose/build.go
@@ -136,7 +136,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
-	project, err := opts.ToProject(dockerCli, services, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
+	project, err := opts.ToProject(ctx, dockerCli, services, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/completion.go b/cmd/compose/completion.go
index 83b233f1e..21351d254 100644
--- a/cmd/compose/completion.go
+++ b/cmd/compose/completion.go
@@ -37,7 +37,7 @@ func noCompletion() validArgsFn {
 func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		p.Offline = true
-		project, err := p.ToProject(dockerCli, nil)
+		project, err := p.ToProject(cmd.Context(), dockerCli, nil)
 		if err != nil {
 			return nil, cobra.ShellCompDirectiveNoFileComp
 		}
@@ -72,7 +72,7 @@ func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []s
 func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		p.Offline = true
-		project, err := p.ToProject(dockerCli, nil)
+		project, err := p.ToProject(cmd.Context(), dockerCli, nil)
 		if err != nil {
 			return nil, cobra.ShellCompDirectiveNoFileComp
 		}
diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go
index 17e5e071c..5d3d62916 100644
--- a/cmd/compose/compose.go
+++ b/cmd/compose/compose.go
@@ -35,18 +35,17 @@ import (
 	dockercli "github.com/docker/cli/cli"
 	"github.com/docker/cli/cli-plugins/manager"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/cmd/formatter"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
+	ui "github.com/docker/compose/v2/pkg/progress"
 	"github.com/docker/compose/v2/pkg/remote"
+	"github.com/docker/compose/v2/pkg/utils"
 	buildkit "github.com/moby/buildkit/util/progress/progressui"
 	"github.com/morikuni/aec"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-
-	"github.com/docker/compose/v2/cmd/formatter"
-	"github.com/docker/compose/v2/pkg/api"
-	"github.com/docker/compose/v2/pkg/compose"
-	ui "github.com/docker/compose/v2/pkg/progress"
-	"github.com/docker/compose/v2/pkg/utils"
 )
 
 const (
@@ -140,10 +139,9 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
 		options := []cli.ProjectOptionsFn{
 			cli.WithResolvedPaths(true),
 			cli.WithDiscardEnvFile,
-			cli.WithContext(ctx),
 		}
 
-		project, err := o.ToProject(dockerCli, args, options...)
+		project, err := o.ToProject(ctx, dockerCli, args, options...)
 		if err != nil {
 			return err
 		}
@@ -164,11 +162,11 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
 	_ = f.MarkHidden("workdir")
 }
 
-func (o *ProjectOptions) projectOrName(dockerCli command.Cli, services ...string) (*types.Project, string, error) {
+func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
 	name := o.ProjectName
 	var project *types.Project
 	if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
-		p, err := o.ToProject(dockerCli, services, cli.WithDiscardEnvFile)
+		p, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile)
 		if err != nil {
 			envProjectName := os.Getenv(ComposeProjectName)
 			if envProjectName != "" {
@@ -182,7 +180,7 @@ func (o *ProjectOptions) projectOrName(dockerCli command.Cli, services ...string
 	return project, name, nil
 }
 
-func (o *ProjectOptions) toProjectName(dockerCli command.Cli) (string, error) {
+func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) {
 	if o.ProjectName != "" {
 		return o.ProjectName, nil
 	}
@@ -192,18 +190,20 @@ func (o *ProjectOptions) toProjectName(dockerCli command.Cli) (string, error) {
 		return envProjectName, nil
 	}
 
-	project, err := o.ToProject(dockerCli, nil)
+	project, err := o.ToProject(ctx, dockerCli, nil)
 	if err != nil {
 		return "", err
 	}
 	return project.Name, nil
 }
 
-func (o *ProjectOptions) ToProject(dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
+func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
 	if !o.Offline {
-		po = o.configureRemoteLoaders(dockerCli, po)
+		po = append(po, o.remoteLoaders(dockerCli)...)
 	}
 
+	po = append(po, cli.WithContext(ctx))
+
 	options, err := o.toProjectOptions(po...)
 	if err != nil {
 		return nil, compose.WrapComposeError(err)
@@ -248,12 +248,10 @@ func (o *ProjectOptions) ToProject(dockerCli command.Cli, services []string, po
 	return project, err
 }
 
-func (o *ProjectOptions) configureRemoteLoaders(dockerCli command.Cli, po []cli.ProjectOptionsFn) []cli.ProjectOptionsFn {
+func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []cli.ProjectOptionsFn {
 	git := remote.NewGitRemoteLoader(o.Offline)
 	oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
-
-	po = append(po, cli.WithResourceLoader(git), cli.WithResourceLoader(oci))
-	return po
+	return []cli.ProjectOptionsFn{cli.WithResourceLoader(git), cli.WithResourceLoader(oci)}
 }
 
 func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
diff --git a/cmd/compose/config.go b/cmd/compose/config.go
index cf26832cf..b0386be72 100644
--- a/cmd/compose/config.go
+++ b/cmd/compose/config.go
@@ -57,9 +57,8 @@ func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, se
 		cli.WithNormalization(!o.noNormalize),
 		cli.WithConsistency(!o.noConsistency),
 		cli.WithDefaultProfiles(o.Profiles...),
-		cli.WithDiscardEnvFile,
-		cli.WithContext(ctx))
-	return o.ProjectOptions.ToProject(dockerCli, services, po...)
+		cli.WithDiscardEnvFile)
+	return o.ProjectOptions.ToProject(ctx, dockerCli, services, po...)
 }
 
 func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
diff --git a/cmd/compose/cp.go b/cmd/compose/cp.go
index 4c346b2a7..ee7c5a9d6 100644
--- a/cmd/compose/cp.go
+++ b/cmd/compose/cp.go
@@ -76,7 +76,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
-	name, err := opts.toProjectName(dockerCli)
+	name, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/down.go b/cmd/compose/down.go
index 97215a785..4343d7870 100644
--- a/cmd/compose/down.go
+++ b/cmd/compose/down.go
@@ -78,7 +78,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/events.go b/cmd/compose/events.go
index 10ef5b442..cfd90d03d 100644
--- a/cmd/compose/events.go
+++ b/cmd/compose/events.go
@@ -52,7 +52,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
 }
 
 func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error {
-	name, err := opts.toProjectName(dockerCli)
+	name, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/exec.go b/cmd/compose/exec.go
index 12ebc7005..1fdd6e731 100644
--- a/cmd/compose/exec.go
+++ b/cmd/compose/exec.go
@@ -82,7 +82,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
-	projectName, err := opts.toProjectName(dockerCli)
+	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/images.go b/cmd/compose/images.go
index d10700767..fd120544e 100644
--- a/cmd/compose/images.go
+++ b/cmd/compose/images.go
@@ -57,7 +57,7 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
 }
 
 func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
-	projectName, err := opts.toProjectName(dockerCli)
+	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/kill.go b/cmd/compose/kill.go
index d38201cbc..358eaec7a 100644
--- a/cmd/compose/kill.go
+++ b/cmd/compose/kill.go
@@ -55,7 +55,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/logs.go b/cmd/compose/logs.go
index 2df7bb59f..62a7224c1 100644
--- a/cmd/compose/logs.go
+++ b/cmd/compose/logs.go
@@ -71,7 +71,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/pause.go b/cmd/compose/pause.go
index acfc0dec5..6f3457719 100644
--- a/cmd/compose/pause.go
+++ b/cmd/compose/pause.go
@@ -45,7 +45,7 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
@@ -76,7 +76,7 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
 }
 
 func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/port.go b/cmd/compose/port.go
index 0baa875c3..d63b03ec4 100644
--- a/cmd/compose/port.go
+++ b/cmd/compose/port.go
@@ -63,7 +63,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
-	projectName, err := opts.toProjectName(dockerCli)
+	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go
index 8b3f3d34d..5bc131df3 100644
--- a/cmd/compose/ps.go
+++ b/cmd/compose/ps.go
@@ -92,7 +92,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
 }
 
 func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go
index 2ef14aabf..32a202085 100644
--- a/cmd/compose/publish.go
+++ b/cmd/compose/publish.go
@@ -50,7 +50,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
 }
 
 func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
-	project, err := opts.ToProject(dockerCli, nil)
+	project, err := opts.ToProject(ctx, dockerCli, nil)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/pull.go b/cmd/compose/pull.go
index d6ce561be..3f350ee86 100644
--- a/cmd/compose/pull.go
+++ b/cmd/compose/pull.go
@@ -94,7 +94,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
 }
 
 func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
-	project, err := opts.ToProject(dockerCli, services)
+	project, err := opts.ToProject(ctx, dockerCli, services)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/push.go b/cmd/compose/push.go
index ccc0e5991..603c2092f 100644
--- a/cmd/compose/push.go
+++ b/cmd/compose/push.go
@@ -54,7 +54,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error {
-	project, err := opts.ToProject(dockerCli, services)
+	project, err := opts.ToProject(ctx, dockerCli, services)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/remove.go b/cmd/compose/remove.go
index 46ac7b13f..adcd3663c 100644
--- a/cmd/compose/remove.go
+++ b/cmd/compose/remove.go
@@ -60,7 +60,7 @@ Any data which is not in a volume will be lost.`,
 }
 
 func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/restart.go b/cmd/compose/restart.go
index 9fe65f461..aeb44c86c 100644
--- a/cmd/compose/restart.go
+++ b/cmd/compose/restart.go
@@ -56,7 +56,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
 }
 
 func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli)
+	project, name, err := opts.projectOrName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/run.go b/cmd/compose/run.go
index c2649d5f1..19db6e32e 100644
--- a/cmd/compose/run.go
+++ b/cmd/compose/run.go
@@ -156,7 +156,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			project, err := p.ToProject(dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
+			project, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
 			if err != nil {
 				return err
 			}
diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go
index 2ab435127..ea852dabb 100644
--- a/cmd/compose/scale.go
+++ b/cmd/compose/scale.go
@@ -61,7 +61,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 
 func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
 	services := maps.Keys(serviceReplicaTuples)
-	project, err := opts.ToProject(dockerCli, services)
+	project, err := opts.ToProject(ctx, dockerCli, services)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/start.go b/cmd/compose/start.go
index 682a78906..6bde4b104 100644
--- a/cmd/compose/start.go
+++ b/cmd/compose/start.go
@@ -44,7 +44,7 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/stats.go b/cmd/compose/stats.go
index c6c2f7fed..2e503fe5d 100644
--- a/cmd/compose/stats.go
+++ b/cmd/compose/stats.go
@@ -63,7 +63,7 @@ Refer to https://docs.docker.com/go/formatting/ for more information about forma
 }
 
 func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error {
-	name, err := opts.ProjectOptions.toProjectName(dockerCli)
+	name, err := opts.ProjectOptions.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/stop.go b/cmd/compose/stop.go
index 2818ef4f3..d06cf6f22 100644
--- a/cmd/compose/stop.go
+++ b/cmd/compose/stop.go
@@ -54,7 +54,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
-	project, name, err := opts.projectOrName(dockerCli, services...)
+	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/top.go b/cmd/compose/top.go
index fb7dd30d4..9d84c57a9 100644
--- a/cmd/compose/top.go
+++ b/cmd/compose/top.go
@@ -50,7 +50,7 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
 }
 
 func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
-	projectName, err := opts.toProjectName(dockerCli)
+	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/viz.go b/cmd/compose/viz.go
index 2b4d65073..4410136f5 100644
--- a/cmd/compose/viz.go
+++ b/cmd/compose/viz.go
@@ -65,7 +65,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
 
 func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
 	_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
-	project, err := opts.ToProject(dockerCli, nil)
+	project, err := opts.ToProject(ctx, dockerCli, nil)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/wait.go b/cmd/compose/wait.go
index e95818b95..88c939140 100644
--- a/cmd/compose/wait.go
+++ b/cmd/compose/wait.go
@@ -61,7 +61,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
-	_, name, err := opts.projectOrName(dockerCli)
+	_, name, err := opts.projectOrName(ctx, dockerCli)
 	if err != nil {
 		return 0, err
 	}
diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go
index 24d704570..cf39a2075 100644
--- a/cmd/compose/watch.go
+++ b/cmd/compose/watch.go
@@ -63,7 +63,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 }
 
 func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
-	project, err := watchOpts.ToProject(dockerCli, nil)
+	project, err := watchOpts.ToProject(ctx, dockerCli, nil)
 	if err != nil {
 		return err
 	}
diff --git a/go.mod b/go.mod
index 548532d74..3c323949b 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/Microsoft/go-winio v0.6.1
 	github.com/buger/goterm v1.0.4
-	github.com/compose-spec/compose-go/v2 v2.0.0-rc.3
+	github.com/compose-spec/compose-go/v2 v2.0.0-rc.4
 	github.com/containerd/console v1.0.3
 	github.com/containerd/containerd v1.7.12
 	github.com/davecgh/go-spew v1.1.1
@@ -44,7 +44,7 @@ require (
 	go.opentelemetry.io/otel/trace v1.19.0
 	go.uber.org/goleak v1.3.0
 	go.uber.org/mock v0.4.0
-	golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
+	golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
 	golang.org/x/sync v0.6.0
 	golang.org/x/sys v0.16.0
 	google.golang.org/grpc v1.59.0
@@ -154,14 +154,14 @@ require (
 	go.opentelemetry.io/otel/metric v1.19.0 // indirect
 	go.opentelemetry.io/otel/sdk/metric v1.19.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
-	golang.org/x/crypto v0.17.0 // indirect
-	golang.org/x/mod v0.11.0 // indirect
-	golang.org/x/net v0.17.0 // indirect
+	golang.org/x/crypto v0.18.0 // indirect
+	golang.org/x/mod v0.14.0 // indirect
+	golang.org/x/net v0.20.0 // indirect
 	golang.org/x/oauth2 v0.11.0 // indirect
-	golang.org/x/term v0.15.0 // indirect
+	golang.org/x/term v0.16.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
-	golang.org/x/tools v0.10.0 // indirect
+	golang.org/x/tools v0.17.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
diff --git a/go.sum b/go.sum
index b8023e388..da37f6054 100644
--- a/go.sum
+++ b/go.sum
@@ -86,8 +86,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g
 github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 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.0.0-rc.3 h1:t0qajSNkH3zR4HEN2CM+GVU7GBx5AwqiYJk5w800M7w=
-github.com/compose-spec/compose-go/v2 v2.0.0-rc.3/go.mod h1:r7CJHU0GaLtRVLm2ch8RCNkJh3GHyaqqc2rSti7VP44=
+github.com/compose-spec/compose-go/v2 v2.0.0-rc.4 h1:vVDWv1xm7Lqi7sEevU0Jk+T2huxNzf92lFhqKbn3sT8=
+github.com/compose-spec/compose-go/v2 v2.0.0-rc.4/go.mod h1:IbZsys5a7eFTsdcWvM3brnghkK7ctZwUSr7mCj5NXu0=
 github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
 github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
@@ -537,19 +537,19 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
-golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
+golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
-golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -564,8 +564,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
 golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
@@ -606,8 +606,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -626,8 +626,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
-golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
+golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/tracing/attributes.go b/internal/tracing/attributes.go
index 1fd2dd7f9..a603ec910 100644
--- a/internal/tracing/attributes.go
+++ b/internal/tracing/attributes.go
@@ -17,6 +17,9 @@
 package tracing
 
 import (
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
 	"strings"
 	"time"
 
@@ -72,6 +75,9 @@ func ProjectOptions(proj *types.Project) SpanOptions {
 		attribute.StringSlice("project.extensions", keys(proj.Extensions)),
 		attribute.StringSlice("project.includes", flattenIncludeReferences(proj.IncludeReferences)),
 	}
+	if projHash, ok := projectHash(proj); ok {
+		attrs = append(attrs, attribute.String("project.hash", projHash))
+	}
 	return []trace.SpanStartEventOption{
 		trace.WithAttributes(attrs...),
 	}
@@ -158,3 +164,23 @@ func flattenIncludeReferences(includeRefs map[string][]types.IncludeConfig) []st
 	}
 	return ret.Elements()
 }
+
+// projectHash returns a checksum from the JSON encoding of the project.
+func projectHash(p *types.Project) (string, bool) {
+	if p == nil {
+		return "", false
+	}
+	// disabled services aren't included in the output, so make a copy with
+	// all the services active for hashing
+	var err error
+	p, err = p.WithServicesEnabled(append(p.ServiceNames(), p.DisabledServiceNames()...)...)
+	if err != nil {
+		return "", false
+	}
+	projData, err := json.Marshal(p)
+	if err != nil {
+		return "", false
+	}
+	d := sha256.Sum256(projData)
+	return fmt.Sprintf("%x", d), true
+}
diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go
index 102dd86ef..f1db51123 100644
--- a/pkg/compose/compose.go
+++ b/pkg/compose/compose.go
@@ -312,11 +312,13 @@ func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
 	return swarmEnabled.val, swarmEnabled.err
 }
 
-var runtimeVersion = struct {
+type runtimeVersionCache struct {
 	once sync.Once
 	val  string
 	err  error
-}{}
+}
+
+var runtimeVersion runtimeVersionCache
 
 func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
 	runtimeVersion.once.Do(func() {
diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go
index f65247b08..e4e3ad188 100644
--- a/pkg/compose/convergence.go
+++ b/pkg/compose/convergence.go
@@ -34,6 +34,7 @@ import (
 	"github.com/docker/compose/v2/internal/tracing"
 	moby "github.com/docker/docker/api/types"
 	containerType "github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/versions"
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/sync/errgroup"
@@ -600,19 +601,27 @@ func (s *composeService) createMobyContainer(ctx context.Context,
 		},
 	}
 
-	// the highest-priority network is the primary and is included in the ContainerCreate API
-	// call via container.NetworkMode & network.NetworkingConfig
-	// any remaining networks are connected one-by-one here after creation (but before start)
-	serviceNetworks := service.NetworksByPriority()
-	for _, networkKey := range serviceNetworks {
-		mobyNetworkName := project.Networks[networkKey].Name
-		if string(cfgs.Host.NetworkMode) == mobyNetworkName {
-			// primary network already configured as part of ContainerCreate
-			continue
-		}
-		epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
-		if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
-			return created, err
+	apiVersion, err := s.RuntimeVersion(ctx)
+	if err != nil {
+		return created, err
+	}
+	// Starting API version 1.44, the ContainerCreate API call takes multiple networks
+	// so we include all the configurations there and can skip the one-by-one calls here
+	if versions.LessThan(apiVersion, "1.44") {
+		// the highest-priority network is the primary and is included in the ContainerCreate API
+		// call via container.NetworkMode & network.NetworkingConfig
+		// any remaining networks are connected one-by-one here after creation (but before start)
+		serviceNetworks := service.NetworksByPriority()
+		for _, networkKey := range serviceNetworks {
+			mobyNetworkName := project.Networks[networkKey].Name
+			if string(cfgs.Host.NetworkMode) == mobyNetworkName {
+				// primary network already configured as part of ContainerCreate
+				continue
+			}
+			epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
+			if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
+				return created, err
+			}
 		}
 	}
 
diff --git a/pkg/compose/create.go b/pkg/compose/create.go
index 069db7cfc..c034b1466 100644
--- a/pkg/compose/create.go
+++ b/pkg/compose/create.go
@@ -235,7 +235,11 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
 	if err != nil {
 		return createConfigs{}, err
 	}
-	networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases)
+	apiVersion, err := s.RuntimeVersion(ctx)
+	if err != nil {
+		return createConfigs{}, err
+	}
+	networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion)
 	portBindings := buildContainerPortBindingOptions(service)
 
 	// MISC
@@ -456,6 +460,7 @@ func defaultNetworkSettings(
 	serviceIndex int,
 	links []string,
 	useNetworkAliases bool,
+	version string,
 ) (container.NetworkMode, *network.NetworkingConfig) {
 	if service.NetworkMode != "" {
 		return container.NetworkMode(service.NetworkMode), nil
@@ -465,23 +470,38 @@ func defaultNetworkSettings(
 		return "none", nil
 	}
 
-	var networkKey string
+	var primaryNetworkKey string
 	if len(service.Networks) > 0 {
-		networkKey = service.NetworksByPriority()[0]
+		primaryNetworkKey = service.NetworksByPriority()[0]
 	} else {
-		networkKey = "default"
+		primaryNetworkKey = "default"
+	}
+	primaryNetworkMobyNetworkName := project.Networks[primaryNetworkKey].Name
+	endpointsConfig := map[string]*network.EndpointSettings{
+		primaryNetworkMobyNetworkName: createEndpointSettings(project, service, serviceIndex, primaryNetworkKey, links, useNetworkAliases),
+	}
+
+	// Starting from API version 1.44, the Engine will take several EndpointsConfigs
+	// so we can pass all the extra networks we want the container to be connected to
+	// in the network configuration instead of connecting the container to each extra
+	// network individually after creation.
+	if versions.GreaterThanOrEqualTo(version, "1.44") && len(service.Networks) > 1 {
+		serviceNetworks := service.NetworksByPriority()
+		for _, networkKey := range serviceNetworks[1:] {
+			mobyNetworkName := project.Networks[networkKey].Name
+			epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
+			endpointsConfig[mobyNetworkName] = epSettings
+		}
 	}
-	mobyNetworkName := project.Networks[networkKey].Name
-	epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
+
 	networkConfig := &network.NetworkingConfig{
-		EndpointsConfig: map[string]*network.EndpointSettings{
-			mobyNetworkName: epSettings,
-		},
+		EndpointsConfig: endpointsConfig,
 	}
+
 	// From the Engine API docs:
 	// > Supported standard values are: bridge, host, none, and container:<name|id>.
 	// > Any other value is taken as a custom network's name to which this container should connect to.
-	return container.NetworkMode(mobyNetworkName), networkConfig
+	return container.NetworkMode(primaryNetworkMobyNetworkName), networkConfig
 }
 
 func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {


Test patch:
diff --git a/internal/tracing/attributes_test.go b/internal/tracing/attributes_test.go
new file mode 100644
index 000000000..d4277a940
--- /dev/null
+++ b/internal/tracing/attributes_test.go
@@ -0,0 +1,67 @@
+/*
+   Copyright 2024 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package tracing
+
+import (
+	"testing"
+
+	"github.com/compose-spec/compose-go/v2/types"
+	"github.com/stretchr/testify/require"
+)
+
+func TestProjectHash(t *testing.T) {
+	projA := &types.Project{
+		Name:       "fake-proj",
+		WorkingDir: "/tmp",
+		Services: map[string]types.ServiceConfig{
+			"foo": {Image: "fake-image"},
+		},
+		DisabledServices: map[string]types.ServiceConfig{
+			"bar": {Image: "diff-image"},
+		},
+	}
+	projB := &types.Project{
+		Name:       "fake-proj",
+		WorkingDir: "/tmp",
+		Services: map[string]types.ServiceConfig{
+			"foo": {Image: "fake-image"},
+			"bar": {Image: "diff-image"},
+		},
+	}
+	projC := &types.Project{
+		Name:       "fake-proj",
+		WorkingDir: "/tmp",
+		Services: map[string]types.ServiceConfig{
+			"foo": {Image: "fake-image"},
+			"bar": {Image: "diff-image"},
+			"baz": {Image: "yet-another-image"},
+		},
+	}
+
+	hashA, ok := projectHash(projA)
+	require.True(t, ok)
+	require.NotEmpty(t, hashA)
+	hashB, ok := projectHash(projB)
+	require.True(t, ok)
+	require.NotEmpty(t, hashB)
+	require.Equal(t, hashA, hashB)
+
+	hashC, ok := projectHash(projC)
+	require.True(t, ok)
+	require.NotEmpty(t, hashC)
+	require.NotEqual(t, hashC, hashA)
+}
diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go
index 32917cb66..e25ccd9f6 100644
--- a/pkg/compose/convergence_test.go
+++ b/pkg/compose/convergence_test.go
@@ -23,14 +23,18 @@ import (
 	"testing"
 
 	"github.com/compose-spec/compose-go/v2/types"
+	"github.com/docker/cli/cli/config/configfile"
 	moby "github.com/docker/docker/api/types"
 	containerType "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/network"
+	"github.com/docker/go-connections/nat"
 	"go.uber.org/mock/gomock"
 	"gotest.tools/v3/assert"
 
 	"github.com/docker/compose/v2/pkg/api"
 	"github.com/docker/compose/v2/pkg/mocks"
+	"github.com/docker/compose/v2/pkg/progress"
 )
 
 func TestContainerName(t *testing.T) {
@@ -251,3 +255,182 @@ func TestWaitDependencies(t *testing.T) {
 		assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil))
 	})
 }
+
+func TestCreateMobyContainer(t *testing.T) {
+	t.Run("connects container networks one by one if API <1.44", func(t *testing.T) {
+		mockCtrl := gomock.NewController(t)
+		defer mockCtrl.Finish()
+		apiClient := mocks.NewMockAPIClient(mockCtrl)
+		cli := mocks.NewMockCli(mockCtrl)
+		tested := composeService{
+			dockerCli: cli,
+		}
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
+		cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
+		apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
+		apiClient.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(moby.ImageInspect{}, nil, nil).AnyTimes()
+		// force `RuntimeVersion` to fetch again
+		runtimeVersion = runtimeVersionCache{}
+		apiClient.EXPECT().ServerVersion(gomock.Any()).Return(moby.Version{
+			APIVersion: "1.43",
+		}, nil).AnyTimes()
+
+		service := types.ServiceConfig{
+			Name: "test",
+			Networks: map[string]*types.ServiceNetworkConfig{
+				"a": {
+					Priority: 10,
+				},
+				"b": {
+					Priority: 100,
+				},
+			},
+		}
+		project := types.Project{
+			Name: "bork",
+			Services: types.Services{
+				"test": service,
+			},
+			Networks: types.Networks{
+				"a": types.NetworkConfig{
+					Name: "a-moby-name",
+				},
+				"b": types.NetworkConfig{
+					Name: "b-moby-name",
+				},
+			},
+		}
+
+		var falseBool bool
+		apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq(
+			&containerType.HostConfig{
+				PortBindings: nat.PortMap{},
+				ExtraHosts:   []string{},
+				Tmpfs:        map[string]string{},
+				Resources: containerType.Resources{
+					OomKillDisable: &falseBool,
+				},
+				NetworkMode: "b-moby-name",
+			}), gomock.Eq(
+			&network.NetworkingConfig{
+				EndpointsConfig: map[string]*network.EndpointSettings{
+					"b-moby-name": {
+						IPAMConfig: &network.EndpointIPAMConfig{},
+						Aliases:    []string{"bork-test-0"},
+					},
+				},
+			}), gomock.Any(), gomock.Any()).Times(1).Return(
+			containerType.CreateResponse{
+				ID: "an-id",
+			}, nil)
+
+		apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id")).Times(1).Return(
+			moby.ContainerJSON{
+				ContainerJSONBase: &moby.ContainerJSONBase{
+					ID:   "an-id",
+					Name: "a-name",
+				},
+				Config:          &containerType.Config{},
+				NetworkSettings: &moby.NetworkSettings{},
+			}, nil)
+
+		apiClient.EXPECT().NetworkConnect(gomock.Any(), "a-moby-name", "an-id", gomock.Eq(
+			&network.EndpointSettings{
+				IPAMConfig: &network.EndpointIPAMConfig{},
+				Aliases:    []string{"bork-test-0"},
+			}))
+
+		_, err := tested.createMobyContainer(context.Background(), &project, service, "test", 0, nil, createOptions{
+			Labels: make(types.Labels),
+		}, progress.ContextWriter(context.TODO()))
+		assert.NilError(t, err)
+	})
+
+	t.Run("includes all container networks in ContainerCreate call if API >=1.44", func(t *testing.T) {
+		mockCtrl := gomock.NewController(t)
+		defer mockCtrl.Finish()
+		apiClient := mocks.NewMockAPIClient(mockCtrl)
+		cli := mocks.NewMockCli(mockCtrl)
+		tested := composeService{
+			dockerCli: cli,
+		}
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
+		cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
+		apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
+		apiClient.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(moby.ImageInspect{}, nil, nil).AnyTimes()
+		// force `RuntimeVersion` to fetch fresh version
+		runtimeVersion = runtimeVersionCache{}
+		apiClient.EXPECT().ServerVersion(gomock.Any()).Return(moby.Version{
+			APIVersion: "1.44",
+		}, nil).AnyTimes()
+
+		service := types.ServiceConfig{
+			Name: "test",
+			Networks: map[string]*types.ServiceNetworkConfig{
+				"a": {
+					Priority: 10,
+				},
+				"b": {
+					Priority: 100,
+				},
+			},
+		}
+		project := types.Project{
+			Name: "bork",
+			Services: types.Services{
+				"test": service,
+			},
+			Networks: types.Networks{
+				"a": types.NetworkConfig{
+					Name: "a-moby-name",
+				},
+				"b": types.NetworkConfig{
+					Name: "b-moby-name",
+				},
+			},
+		}
+
+		var falseBool bool
+		apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq(
+			&containerType.HostConfig{
+				PortBindings: nat.PortMap{},
+				ExtraHosts:   []string{},
+				Tmpfs:        map[string]string{},
+				Resources: containerType.Resources{
+					OomKillDisable: &falseBool,
+				},
+				NetworkMode: "b-moby-name",
+			}), gomock.Eq(
+			&network.NetworkingConfig{
+				EndpointsConfig: map[string]*network.EndpointSettings{
+					"a-moby-name": {
+						IPAMConfig: &network.EndpointIPAMConfig{},
+						Aliases:    []string{"bork-test-0"},
+					},
+					"b-moby-name": {
+						IPAMConfig: &network.EndpointIPAMConfig{},
+						Aliases:    []string{"bork-test-0"},
+					},
+				},
+			}), gomock.Any(), gomock.Any()).Times(1).Return(
+			containerType.CreateResponse{
+				ID: "an-id",
+			}, nil)
+
+		apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id")).Times(1).Return(
+			moby.ContainerJSON{
+				ContainerJSONBase: &moby.ContainerJSONBase{
+					ID:   "an-id",
+					Name: "a-name",
+				},
+				Config:          &containerType.Config{},
+				NetworkSettings: &moby.NetworkSettings{},
+			}, nil)
+
+		_, err := tested.createMobyContainer(context.Background(), &project, service, "test", 0, nil, createOptions{
+			Labels: make(types.Labels),
+		}, progress.ContextWriter(context.TODO()))
+		assert.NilError(t, err)
+	})
+
+}
diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go
index 1a4734896..7991d09e8 100644
--- a/pkg/compose/create_test.go
+++ b/pkg/compose/create_test.go
@@ -193,7 +193,7 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			}),
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
+		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
 		assert.Equal(t, string(networkMode), "myProject_myNetwork2")
 		assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
 		assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
@@ -221,7 +221,7 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			}),
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
+		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
 		assert.Equal(t, string(networkMode), "myProject_default")
 		assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
 		assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
@@ -238,7 +238,7 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			},
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
+		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
 		assert.Equal(t, string(networkMode), "none")
 		assert.Check(t, cmp.Nil(networkConfig))
 	})
@@ -258,7 +258,7 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			}),
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
+		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
 		assert.Equal(t, string(networkMode), "host")
 		assert.Check(t, cmp.Nil(networkConfig))
 	})