@@ -144,10 +144,25 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
144144 return entries [i ].service < entries [j ].service
145145 })
146146
147- // Create forwarders
148- var portMappings []state.PortMapping
147+ // Collect existing port mappings to avoid duplicate forwarders
148+ var existingPorts []state.PortMapping
149+ if existing != nil {
150+ existingPorts = existing .Ports
151+ }
152+
153+ // Create forwarders only for NEW ports
154+ var newPortMappings []state.PortMapping
149155 for _ , e := range entries {
150156 listenAddr := fmt .Sprintf ("%s:%d" , ip , e .containerPort )
157+
158+ cancelKey := project + ":" + e .service + ":" + listenAddr
159+
160+ // Skip if a forwarder for this listen address already exists
161+ if _ , exists := d .cancelFns [cancelKey ]; exists {
162+ slog .Debug ("forwarder already active, skipping" , "project" , project , "listen" , listenAddr )
163+ continue
164+ }
165+
151166 targetAddr := fmt .Sprintf ("127.0.0.1:%d" , e .hostPort )
152167
153168 fwdCtx , fwdCancel := context .WithCancel (ctx )
@@ -159,9 +174,9 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
159174 }()
160175
161176 d .forwarders [project ] = append (d .forwarders [project ], fwd )
162- d .cancelFns [project + ":" + listenAddr ] = fwdCancel
177+ d .cancelFns [cancelKey ] = fwdCancel
163178
164- portMappings = append (portMappings , state.PortMapping {
179+ newPortMappings = append (newPortMappings , state.PortMapping {
165180 ContainerPort : e .containerPort ,
166181 HostPort : e .hostPort ,
167182 Service : e .service ,
@@ -170,11 +185,26 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
170185 slog .Info ("forwarding" , "project" , project , "listen" , listenAddr , "target" , targetAddr )
171186 }
172187
173- // Update state
188+ // Merge existing + new port mappings (deduplicated by listen addr)
189+ mergedPorts := existingPorts
190+ for _ , np := range newPortMappings {
191+ duplicate := false
192+ for _ , ep := range existingPorts {
193+ if ep .ContainerPort == np .ContainerPort {
194+ duplicate = true
195+ break
196+ }
197+ }
198+ if ! duplicate {
199+ mergedPorts = append (mergedPorts , np )
200+ }
201+ }
202+
203+ // Update state with merged ports
174204 p := & state.Project {
175205 Name : project ,
176206 IP : ip ,
177- Ports : portMappings ,
207+ Ports : mergedPorts ,
178208 }
179209 d .state .AddProject (p )
180210
@@ -183,26 +213,44 @@ func (d *Daemon) OnContainerStart(ctx context.Context, info watcher.ContainerInf
183213
184214func (d * Daemon ) OnContainerDie (ctx context.Context , info watcher.ContainerInfo ) error {
185215 project := info .Project
216+ service := info .Service
186217
187- // Cancel all forwarders for this project
218+ // Cancel only forwarders belonging to this service
219+ prefix := project + ":" + service + ":"
188220 for key , cancel := range d .cancelFns {
189- if len (key ) > len (project ) && key [:len (project ) + 1 ] == project + ":" {
221+ if len (key ) >= len (prefix ) && key [:len (prefix ) ] == prefix {
190222 cancel ()
191223 delete (d .cancelFns , key )
192224 }
193225 }
194- delete (d .forwarders , project )
195226
227+ // Remove this service's ports from the project state
196228 p := d .state .GetProject (project )
197229 if p == nil {
198230 return nil
199231 }
200232
201- d .dns .Unregister (project )
202- d .ipman .RemoveLoopbackIP (p .IP )
203- d .state .RemoveProject (project )
233+ var remaining []state.PortMapping
234+ for _ , pm := range p .Ports {
235+ if pm .Service != service {
236+ remaining = append (remaining , pm )
237+ }
238+ }
239+
240+ if len (remaining ) == 0 {
241+ // No more services in this project — full teardown
242+ delete (d .forwarders , project )
243+ d .dns .Unregister (project )
244+ d .ipman .RemoveLoopbackIP (p .IP )
245+ d .state .RemoveProject (project )
246+ slog .Info ("project removed" , "project" , project )
247+ } else {
248+ // Update project with remaining ports
249+ p .Ports = remaining
250+ d .state .AddProject (p )
251+ slog .Info ("service removed" , "project" , project , "service" , service , "remaining" , len (remaining ))
252+ }
204253
205- slog .Info ("project removed" , "project" , project )
206254 return nil
207255}
208256
0 commit comments