Repository: docker/compose
Pull request #12771 (2025-04-23 14:22:54)

introduce networks.interface_name
**What I did**

introduce `networks.interface_name` to control virtual network interface assignment to target networks

**Related issue**
closes https://github.com/docker/compose/issues/12740

**(not mandatory) A picture of a cute animal, if possible in relation to what you did**



Patch:
diff --git a/go.mod b/go.mod
index f4bbe5b2f..8a15ba21a 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,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.6.1
+	github.com/compose-spec/compose-go/v2 v2.6.2-0.20250423090706-30ff01d36f76
 	github.com/containerd/containerd/v2 v2.0.5
 	github.com/containerd/platforms v1.0.0-rc.1
 	github.com/davecgh/go-spew v1.1.1
diff --git a/go.sum b/go.sum
index 512a169bf..b4f626f4e 100644
--- a/go.sum
+++ b/go.sum
@@ -82,8 +82,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.6.1 h1:276YiQKRcGGtgkxiymzWHJ2CTv5joQA+7DTNrUA+rys=
-github.com/compose-spec/compose-go/v2 v2.6.1/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
+github.com/compose-spec/compose-go/v2 v2.6.2-0.20250423090706-30ff01d36f76 h1:KZvD41eTRr9/n43zccAcGPBRgzHXdbLZY4IXSeJxqIw=
+github.com/compose-spec/compose-go/v2 v2.6.2-0.20250423090706-30ff01d36f76/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
 github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
 github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
diff --git a/pkg/compose/create.go b/pkg/compose/create.go
index 7e531bf7d..825fe4709 100644
--- a/pkg/compose/create.go
+++ b/pkg/compose/create.go
@@ -154,6 +154,7 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
 	return ids, nil
 }
 
+//nolint:gocyclo
 func (s *composeService) getCreateConfigs(ctx context.Context,
 	p *types.Project,
 	service types.ServiceConfig,
@@ -246,7 +247,10 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
 	if err != nil {
 		return createConfigs{}, err
 	}
-	networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion)
+	networkMode, networkingConfig, err := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion)
+	if err != nil {
+		return createConfigs{}, err
+	}
 	portBindings := buildContainerPortBindingOptions(service)
 
 	// MISC
@@ -356,7 +360,7 @@ func (s *composeService) prepareContainerMACAddress(ctx context.Context, service
 		}
 
 		if len(withMacAddress) > 1 {
-			return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine 1.44 or later (currently: %s)", strings.Join(withMacAddress, ", "), version)
+			return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine v25 or later", strings.Join(withMacAddress, ", "))
 		}
 
 		if mainNw != nil && mainNw.MacAddress != "" {
@@ -379,6 +383,8 @@ func getAliases(project *types.Project, service types.ServiceConfig, serviceInde
 }
 
 func createEndpointSettings(p *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, links []string, useNetworkAliases bool) *network.EndpointSettings {
+	const ifname = "com.docker.network.endpoint.ifname"
+
 	config := service.Networks[networkKey]
 	var ipam *network.EndpointIPAMConfig
 	var (
@@ -398,6 +404,15 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi
 		}
 		macAddress = config.MacAddress
 		driverOpts = config.DriverOpts
+		if config.InterfaceName != "" {
+			if driverOpts == nil {
+				driverOpts = map[string]string{}
+			}
+			if name, ok := driverOpts[ifname]; ok && name != config.InterfaceName {
+				logrus.Warnf("ignoring services.%s.networks.%s.interface_name as %s driver_opts is already declared", service.Name, networkKey, ifname)
+			}
+			driverOpts[ifname] = config.InterfaceName
+		}
 		gwPriority = config.GatewayPriority
 	}
 	return &network.EndpointSettings{
@@ -471,20 +486,17 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
 }
 
 // defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable).
-func defaultNetworkSettings(
-	project *types.Project,
-	service types.ServiceConfig,
-	serviceIndex int,
-	links []string,
-	useNetworkAliases bool,
+func defaultNetworkSettings(project *types.Project,
+	service types.ServiceConfig, serviceIndex int,
+	links []string, useNetworkAliases bool,
 	version string,
-) (container.NetworkMode, *network.NetworkingConfig) {
+) (container.NetworkMode, *network.NetworkingConfig, error) {
 	if service.NetworkMode != "" {
-		return container.NetworkMode(service.NetworkMode), nil
+		return container.NetworkMode(service.NetworkMode), nil, nil
 	}
 
 	if len(project.Networks) == 0 {
-		return "none", nil
+		return "none", nil, nil
 	}
 
 	var primaryNetworkKey string
@@ -515,6 +527,14 @@ func defaultNetworkSettings(
 		}
 	}
 
+	if versions.LessThan(version, "1.49") {
+		for _, config := range service.Networks {
+			if config != nil && config.InterfaceName != "" {
+				return "", nil, fmt.Errorf("interface_name requires Docker Engine v28.1 or later")
+			}
+		}
+	}
+
 	endpointsConfig[primaryNetworkMobyNetworkName] = primaryNetworkEndpoint
 	networkConfig := &network.NetworkingConfig{
 		EndpointsConfig: endpointsConfig,
@@ -523,7 +543,7 @@ func defaultNetworkSettings(
 	// 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(primaryNetworkMobyNetworkName), networkConfig
+	return container.NetworkMode(primaryNetworkMobyNetworkName), networkConfig, nil
 }
 
 func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {


Test patch:
diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go
index bec42beef..3394bf3c9 100644
--- a/pkg/compose/create_test.go
+++ b/pkg/compose/create_test.go
@@ -219,7 +219,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			}),
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		assert.NilError(t, err)
 		assert.Equal(t, string(networkMode), "myProject_myNetwork2")
 		assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
 		assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
@@ -247,7 +248,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			}),
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		assert.NilError(t, err)
 		assert.Equal(t, string(networkMode), "myProject_default")
 		assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
 		assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
@@ -264,7 +266,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			},
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		assert.NilError(t, err)
 		assert.Equal(t, string(networkMode), "none")
 		assert.Check(t, cmp.Nil(networkConfig))
 	})
@@ -284,7 +287,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 			}),
 		}
 
-		networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
+		assert.NilError(t, err)
 		assert.Equal(t, string(networkMode), "host")
 		assert.Check(t, cmp.Nil(networkConfig))
 	})
diff --git a/pkg/e2e/fixtures/network-interface-name/compose.yaml b/pkg/e2e/fixtures/network-interface-name/compose.yaml
new file mode 100644
index 000000000..701830a48
--- /dev/null
+++ b/pkg/e2e/fixtures/network-interface-name/compose.yaml
@@ -0,0 +1,7 @@
+services:
+  test:
+    image: alpine
+    command: ip link show
+    networks:
+      default:
+        interface_name: foobar
diff --git a/pkg/e2e/networks_test.go b/pkg/e2e/networks_test.go
index c74687adc..cbd4e0e71 100644
--- a/pkg/e2e/networks_test.go
+++ b/pkg/e2e/networks_test.go
@@ -181,3 +181,21 @@ func TestMacAddress(t *testing.T) {
 	res := c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-test-1", projectName), "-f", "{{ (index .NetworkSettings.Networks \"network_mac_address_default\" ).MacAddress }}")
 	res.Assert(t, icmd.Expected{Out: "00:e0:84:35:d0:e8"})
 }
+
+func TestInterfaceName(t *testing.T) {
+	c := NewCLI(t)
+
+	version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
+	major, _, found := strings.Cut(version.Combined(), ".")
+	assert.Assert(t, found)
+	if major == "26" || major == "27" {
+		t.Skip("Skipping test due to docker version < 28")
+	}
+
+	const projectName = "network_interface_name"
+	res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-interface-name/compose.yaml", "--project-name", projectName, "run", "test")
+	t.Cleanup(func() {
+		c.cleanupWithDown(t, projectName)
+	})
+	res.Assert(t, icmd.Expected{Out: "foobar@"})
+}