From 1a3e5d18a2b2d9318e50191c00375395df2c14a6 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Wed, 12 Aug 2026 20:03:58 +0500 Subject: [PATCH] compose/loader: keep TCP+UDP when merging same published port Port merge keyed only on Published, so 443/tcp and 443/udp collapsed when combining compose files (e.g. stack config with an env-only override). Include protocol in the merge key. Fixes #6223 Signed-off-by: Dean Chen <862469039@qq.com> --- cli/compose/loader/merge.go | 19 +++++++++++++++++-- cli/compose/loader/merge_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 9d8da3c6d81b..ae0b2eb3b782 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -130,11 +130,21 @@ func toServicePortConfigsMap(s any) (map[any]any, error) { } m := map[any]any{} for _, p := range ports { - m[p.Published] = p + // Key by published port + protocol so TCP and UDP on the same host + // port (e.g. 443/tcp and 443/udp) do not collapse when merging files. + m[servicePortMergeKey(p)] = p } return m, nil } +func servicePortMergeKey(p types.ServicePortConfig) string { + proto := p.Protocol + if proto == "" { + proto = "tcp" + } + return fmt.Sprintf("%d/%s", p.Published, proto) +} + func toServiceVolumeConfigsMap(s any) (map[any]any, error) { volumes, ok := s.([]types.ServiceVolumeConfig) if !ok { @@ -172,7 +182,12 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[any]any) error { for _, v := range m { s = append(s, v.(types.ServicePortConfig)) } - sort.Slice(s, func(i, j int) bool { return s[i].Published < s[j].Published }) + sort.Slice(s, func(i, j int) bool { + if s[i].Published != s[j].Published { + return s[i].Published < s[j].Published + } + return s[i].Protocol < s[j].Protocol + }) dst.Set(reflect.ValueOf(s)) return nil } diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go index 11c1e888ead2..c584d1b172bd 100644 --- a/cli/compose/loader/merge_test.go +++ b/cli/compose/loader/merge_test.go @@ -301,6 +301,30 @@ func TestLoadMultipleServicePorts(t *testing.T) { }, }, }, + { + name: "same_published_tcp_and_udp", + portBase: map[string]any{ + "ports": []any{ + "443:443", + "443:443/udp", + }, + }, + portOverride: map[string]any{}, + expected: []types.ServicePortConfig{ + { + Mode: "ingress", + Published: 443, + Target: 443, + Protocol: "tcp", + }, + { + Mode: "ingress", + Published: 443, + Target: 443, + Protocol: "udp", + }, + }, + }, } for _, tc := range portsCases {