fix: do not add service name as alias on external networks

Previously, getAliases() unconditionally appended the service name as
a network alias when useNetworkAliases was true. This caused containers
to register their service name as an alias on external networks, leaking
internal service discovery names into networks managed outside of
Compose.

Guard the service name alias behind an external-network check: only
append it when the network is not marked as external. Explicitly
configured aliases in the service network config are still passed
through regardless.

To make the behavior change discoverable, `up` now emits one warning
per external network the project is connected to, listing the services
whose service-name alias was skipped. Services that already declare
their own name under networks.<net>.aliases are excluded, so the
warning disappears once the user adopts the workaround. `create` and
`run --use-aliases` are intentionally not warned about: `create` is
rarely used standalone, and `run --use-aliases` produces ephemeral
one-off containers where the warning would be noise.

Fixes #8223

Signed-off-by: Stanislav Zhuk <stasadev@gmail.com>
This commit is contained in:
Stanislav Zhuk 2026-05-08 11:47:57 +03:00
parent 4f69a8c997
commit 49a4cb13b8
No known key found for this signature in database
GPG key ID: 9848A8C703EE80C3
6 changed files with 274 additions and 3 deletions

View file

@ -368,10 +368,13 @@ func (s *composeService) prepareContainerMACAddress(service types.ServiceConfig,
return nil
}
func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, cfg *types.ServiceNetworkConfig, useNetworkAliases bool) []string {
func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, cfg *types.ServiceNetworkConfig, useNetworkAliases bool) []string {
aliases := []string{getContainerName(project.Name, service, serviceIndex)}
if useNetworkAliases {
aliases = append(aliases, service.Name)
// service name is not registered as an alias on external networks; see warnExternalNetworkAliases
if n := project.Networks[networkKey]; !n.External {
aliases = append(aliases, service.Name)
}
if cfg != nil {
aliases = append(aliases, cfg.Aliases...)
}
@ -445,7 +448,7 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi
}
return &network.EndpointSettings{
Aliases: getAliases(p, service, serviceIndex, config, useNetworkAliases),
Aliases: getAliases(p, service, serviceIndex, networkKey, config, useNetworkAliases),
Links: links,
IPAddress: ipv4Address,
IPv6Gateway: ipv6Address,

View file

@ -365,6 +365,26 @@ func TestCreateEndpointSettings(t *testing.T) {
}, cmpopts.EquateComparable(netip.Addr{})))
}
func TestCreateEndpointSettings_ExternalNetwork(t *testing.T) {
eps, err := createEndpointSettings(&composetypes.Project{
Name: "projName",
Networks: composetypes.Networks{
"netName": {External: true},
},
}, composetypes.ServiceConfig{
Name: "serviceName",
ContainerName: "containerName",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"netName": {
Aliases: []string{"alias1"},
},
},
}, 0, "netName", nil, true)
assert.NilError(t, err)
assert.Check(t, cmp.DeepEqual(eps.Aliases, []string{"containerName", "alias1"}),
"service name must not be added as alias on external networks")
}
func Test_buildContainerVolumes(t *testing.T) {
pwd, err := os.Getwd()
assert.NilError(t, err)

View file

@ -23,6 +23,7 @@ import (
"os"
"os/signal"
"slices"
"strings"
"sync"
"sync/atomic"
"syscall"
@ -42,6 +43,7 @@ import (
)
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
warnExternalNetworkAliases(project)
err := Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
err := s.create(ctx, project, options.Create)
if err != nil {
@ -314,3 +316,35 @@ func shouldFollowStartEvent(event api.ContainerEvent, attached []string, attachT
}
return true
}
// warnExternalNetworkAliases emits one warning per external network the project is connected to, listing the services
// whose service-name alias is intentionally not registered (see getAliases). Services that already include their own
// name under the network's explicit aliases are excluded, so the warning disappears once the user adopts the
// workaround. Must be called before Run() to avoid interleaving with the TUI progress display.
func warnExternalNetworkAliases(project *types.Project) {
byNetwork := map[string][]string{}
for _, service := range project.Services {
for networkKey, cfg := range service.Networks {
n := project.Networks[networkKey]
if !n.External {
continue
}
if cfg != nil && slices.Contains(cfg.Aliases, service.Name) {
continue
}
byNetwork[networkKey] = append(byNetwork[networkKey], service.Name)
}
}
networks := make([]string, 0, len(byNetwork))
for k := range byNetwork {
networks = append(networks, k)
}
slices.Sort(networks)
for _, networkKey := range networks {
services := byNetwork[networkKey]
slices.Sort(services)
logrus.Warnf("service names [%s] not registered as aliases on external "+
"network %q; add them under each service's networks.%s.aliases if needed",
strings.Join(services, ", "), networkKey, networkKey)
}
}

View file

@ -17,8 +17,12 @@
package compose
import (
"bytes"
"strings"
"testing"
composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
@ -102,3 +106,132 @@ func TestShouldFollowStartEvent(t *testing.T) {
})
}
}
func TestWarnExternalNetworkAliases(t *testing.T) {
tests := []struct {
name string
project *composetypes.Project
expected []string
notExpected []string
}{
{
name: "internal-only network emits no warning",
project: &composetypes.Project{
Networks: composetypes.Networks{"internal": {}},
Services: composetypes.Services{
"web": {
Name: "web",
Networks: map[string]*composetypes.ServiceNetworkConfig{"internal": nil},
},
},
},
notExpected: []string{"not registered as aliases"},
},
{
name: "external network emits one warning listing services in sorted order",
project: &composetypes.Project{
Networks: composetypes.Networks{"shared": {External: true}},
Services: composetypes.Services{
"web": {
Name: "web",
Networks: map[string]*composetypes.ServiceNetworkConfig{"shared": nil},
},
"db": {
Name: "db",
Networks: map[string]*composetypes.ServiceNetworkConfig{"shared": nil},
},
},
},
expected: []string{
`service names [db, web] not registered as aliases on external network`,
`networks.shared.aliases`,
},
},
{
name: "service with explicit self-alias is excluded from the warning",
project: &composetypes.Project{
Networks: composetypes.Networks{"shared": {External: true}},
Services: composetypes.Services{
"web": {
Name: "web",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"shared": {Aliases: []string{"web"}},
},
},
"db": {
Name: "db",
Networks: map[string]*composetypes.ServiceNetworkConfig{"shared": nil},
},
},
},
expected: []string{
`service names [db] not registered as aliases on external network`,
`networks.shared.aliases`,
},
notExpected: []string{"web"},
},
{
name: "all services explicitly self-aliased emits no warning",
project: &composetypes.Project{
Networks: composetypes.Networks{"shared": {External: true}},
Services: composetypes.Services{
"web": {
Name: "web",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"shared": {Aliases: []string{"web"}},
},
},
},
},
notExpected: []string{"not registered as aliases"},
},
{
name: "multiple external networks each emit their own warning",
project: &composetypes.Project{
Networks: composetypes.Networks{
"sharedA": {External: true},
"sharedB": {External: true},
},
Services: composetypes.Services{
"web": {
Name: "web",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"sharedA": nil,
"sharedB": nil,
},
},
},
},
expected: []string{
`networks.sharedA.aliases`,
`networks.sharedB.aliases`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
origOut := logrus.StandardLogger().Out
origLevel := logrus.GetLevel()
origFormatter := logrus.StandardLogger().Formatter
logrus.SetOutput(&buf)
logrus.SetLevel(logrus.WarnLevel)
logrus.SetFormatter(&logrus.TextFormatter{DisableColors: true, DisableTimestamp: true})
t.Cleanup(func() {
logrus.SetOutput(origOut)
logrus.SetLevel(origLevel)
logrus.SetFormatter(origFormatter)
})
warnExternalNetworkAliases(tt.project)
out := buf.String()
for _, s := range tt.expected {
assert.Assert(t, strings.Contains(out, s), "expected %q in output:\n%s", s, out)
}
for _, s := range tt.notExpected {
assert.Assert(t, !strings.Contains(out, s), "did not expect %q in output:\n%s", s, out)
}
})
}
}

View file

@ -0,0 +1,21 @@
networks:
default: {}
external-net:
external: true
name: ${EXTERNAL_NETWORK}
services:
web:
image: busybox
command: sleep infinity
networks:
- default
- external-net
db:
image: busybox
command: sleep infinity
networks:
default: {}
external-net:
aliases:
- db

View file

@ -219,3 +219,63 @@ func TestNetworkRecreate(t *testing.T) {
t.Fatalf("unexpected output, missing expected events, stderr: %s", err)
}
}
func TestExternalNetworkAliases(t *testing.T) {
const projectName = "network_external_alias_e2e"
const externalNet = projectName + "_external"
c := NewParallelCLI(t, WithEnv("EXTERNAL_NETWORK="+externalNet))
c.RunDockerOrExitError(t, "network", "rm", externalNet)
c.RunDockerCmd(t, "network", "create", externalNet)
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
c.RunDockerOrExitError(t, "network", "rm", externalNet)
})
upRes := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-external-alias/compose.yaml",
"--project-name", projectName,
"up", "-d")
internalNet := projectName + "_default"
t.Run("warning lists services without explicit external-net alias and excludes self-aliased ones", func(t *testing.T) {
var warningLine string
for line := range strings.SplitSeq(upRes.Combined(), "\n") {
if strings.Contains(line, `not registered as aliases on external network`) {
warningLine = line
break
}
}
assert.Assert(t, warningLine != "", "expected warning line in output:\n%s", upRes.Combined())
assert.Assert(t, strings.Contains(warningLine, "web"), warningLine)
assert.Assert(t, strings.Contains(warningLine, "external-net"), warningLine)
assert.Assert(t, !strings.Contains(warningLine, "db"),
"db declares its own external-net alias and must be excluded from the warning: %s", warningLine)
})
t.Run("service name is an alias on internal network", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-web-1", "-f",
fmt.Sprintf(`{{ range (index .NetworkSettings.Networks %q).Aliases }}[{{ . }}]{{ end }}`, internalNet))
assert.Assert(t, strings.Contains(res.Stdout(), "[web]"), res.Stdout())
})
t.Run("service name is not an alias on external network", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-web-1", "-f",
fmt.Sprintf(`{{ range (index .NetworkSettings.Networks %q).Aliases }}[{{ . }}]{{ end }}`, externalNet))
assert.Assert(t, !strings.Contains(res.Stdout(), "[web]"), res.Stdout())
})
t.Run("explicit alias under networks.<external>.aliases is registered on external network", func(t *testing.T) {
res := c.RunDockerCmd(t, "inspect", projectName+"-db-1", "-f",
fmt.Sprintf(`{{ range (index .NetworkSettings.Networks %q).Aliases }}[{{ . }}]{{ end }}`, externalNet))
assert.Assert(t, strings.Contains(res.Stdout(), "[db]"), res.Stdout())
})
t.Run("service name resolves on internal network", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-external-alias/compose.yaml",
"--project-name", projectName,
"exec", "-T", "web", "ping", "-c1", "db")
assert.Assert(t, strings.Contains(res.Combined(), "1 packets transmitted"), res.Combined())
})
}