fix(preview): move network removal after container removal
The container runtime (Podman/Docker) refuses to delete a network that still has containers attached, even if stopped. Move network removal to after container removal in the orphan GC flow. Order is now: stop → cleanup volumes/route → remove container → remove network.
- commit
- b035b020e646759e7ad62683e86f7a82789c2a81
- author
- brice · · 2026-03-25 14:12 UTC
- committer
- brice · · 2026-03-25 16:07 UTC
- parent
- 7568a1c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +17 -3
M internal/app/run.go +17 -3
| @@ -645,8 +645,9 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain | |||
| 645 | 645 | log.Warn().Str("container", c.Name).Str("image", c.Image).Str("domain", orphanDomain). | |
| 646 | 646 | Time("created", c.Created).Msg("orphaned preview container detected, cleaning up") | |
| 647 | 647 | ||
| 648 | - | // Stop container first, then clean up domain resources while container | |
| 649 | - | // still exists (so future scans can rediscover it on failure), then remove. | |
| 648 | + | // Order: stop → remove volumes/route → remove container → remove network. | |
| 649 | + | // Network removal must happen after container removal because the runtime | |
| 650 | + | // refuses to delete a network that still has containers attached. | |
| 650 | 651 | if err := svc.runtime.StopContainer(ctx, c.Name); err != nil { | |
| 651 | 652 | log.Warn().Err(err).Str("container", c.Name).Msg("failed to stop orphan container") | |
| 652 | 653 | } | |
| @@ -663,8 +664,22 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain | |||
| 663 | 664 | log.Warn().Err(err).Str("container", c.Name).Msg("failed to remove orphan container") | |
| 664 | 665 | } | |
| 665 | 666 | ||
| 667 | + | // Remove network after container is gone. | |
| 668 | + | if orphanDomain != "" { | |
| 669 | + | if err := removeOrphanNetwork(ctx, svc, strings.ReplaceAll(orphanDomain, ".", "-"), log); err != nil { | |
| 670 | + | log.Warn().Err(err).Str("container", c.Name).Msg("failed to remove orphan network after container removal") | |
| 671 | + | } | |
| 672 | + | } | |
| 673 | + | ||
| 666 | 674 | log.Info().Str("container", c.Name).Str("domain", orphanDomain).Msg("orphaned preview container cleaned up") | |
| 667 | 675 | } | |
| 676 | + | ||
| 677 | + | // Re-sync container state so stale routes disappear from routes list. | |
| 678 | + | if len(orphans) > 0 { | |
| 679 | + | if err := svc.containerSvc.SyncContainers(ctx); err != nil { | |
| 680 | + | log.Warn().Err(err).Msg("failed to sync containers after orphan GC") | |
| 681 | + | } | |
| 682 | + | } | |
| 668 | 683 | } | |
| 669 | 684 | ||
| 670 | 685 | // cleanupOrphanDomainResources removes volumes, network, and route for an | |
| @@ -676,7 +691,6 @@ func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDoma | |||
| 676 | 691 | var errs []error | |
| 677 | 692 | ||
| 678 | 693 | errs = append(errs, removeOrphanVolumes(ctx, svc, domainSanitized, log)...) | |
| 679 | - | errs = append(errs, removeOrphanNetwork(ctx, svc, domainSanitized, log)) | |
| 680 | 694 | errs = append(errs, removeOrphanRoute(ctx, svc, orphanDomain, log)) | |
| 681 | 695 | svc.proxySvc.InvalidateTarget(ctx, orphanDomain) | |
| 682 | 696 | ||
| @@ -645,8 +645,9 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain | |||||
| 645 | log.Warn().Str("container", c.Name).Str("image", c.Image).Str("domain", orphanDomain). | 645 | log.Warn().Str("container", c.Name).Str("image", c.Image).Str("domain", orphanDomain). | ||
| 646 | Time("created", c.Created).Msg("orphaned preview container detected, cleaning up") | 646 | Time("created", c.Created).Msg("orphaned preview container detected, cleaning up") | ||
| 647 | 647 | ||||
| 648 | - | // Stop container first, then clean up domain resources while container | 648 | + | // Order: stop → remove volumes/route → remove container → remove network. |
| 649 | - | // still exists (so future scans can rediscover it on failure), then remove. | 649 | + | // Network removal must happen after container removal because the runtime |
| 650 | + | // refuses to delete a network that still has containers attached. | |||
| 650 | if err := svc.runtime.StopContainer(ctx, c.Name); err != nil { | 651 | if err := svc.runtime.StopContainer(ctx, c.Name); err != nil { | ||
| 651 | log.Warn().Err(err).Str("container", c.Name).Msg("failed to stop orphan container") | 652 | log.Warn().Err(err).Str("container", c.Name).Msg("failed to stop orphan container") | ||
| 652 | } | 653 | } | ||
| @@ -663,8 +664,22 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain | |||||
| 663 | log.Warn().Err(err).Str("container", c.Name).Msg("failed to remove orphan container") | 664 | log.Warn().Err(err).Str("container", c.Name).Msg("failed to remove orphan container") | ||
| 664 | } | 665 | } | ||
| 665 | 666 | ||||
| 667 | + | // Remove network after container is gone. | |||
| 668 | + | if orphanDomain != "" { | |||
| 669 | + | if err := removeOrphanNetwork(ctx, svc, strings.ReplaceAll(orphanDomain, ".", "-"), log); err != nil { | |||
| 670 | + | log.Warn().Err(err).Str("container", c.Name).Msg("failed to remove orphan network after container removal") | |||
| 671 | + | } | |||
| 672 | + | } | |||
| 673 | + | ||||
| 666 | log.Info().Str("container", c.Name).Str("domain", orphanDomain).Msg("orphaned preview container cleaned up") | 674 | log.Info().Str("container", c.Name).Str("domain", orphanDomain).Msg("orphaned preview container cleaned up") | ||
| 667 | } | 675 | } | ||
| 676 | + | ||||
| 677 | + | // Re-sync container state so stale routes disappear from routes list. | |||
| 678 | + | if len(orphans) > 0 { | |||
| 679 | + | if err := svc.containerSvc.SyncContainers(ctx); err != nil { | |||
| 680 | + | log.Warn().Err(err).Msg("failed to sync containers after orphan GC") | |||
| 681 | + | } | |||
| 682 | + | } | |||
| 668 | } | 683 | } | ||
| 669 | 684 | ||||
| 670 | // cleanupOrphanDomainResources removes volumes, network, and route for an | 685 | // cleanupOrphanDomainResources removes volumes, network, and route for an | ||
| @@ -676,7 +691,6 @@ func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDoma | |||||
| 676 | var errs []error | 691 | var errs []error | ||
| 677 | 692 | ||||
| 678 | errs = append(errs, removeOrphanVolumes(ctx, svc, domainSanitized, log)...) | 693 | errs = append(errs, removeOrphanVolumes(ctx, svc, domainSanitized, log)...) | ||
| 679 | - | errs = append(errs, removeOrphanNetwork(ctx, svc, domainSanitized, log)) | |||
| 680 | errs = append(errs, removeOrphanRoute(ctx, svc, orphanDomain, log)) | 694 | errs = append(errs, removeOrphanRoute(ctx, svc, orphanDomain, log)) | ||
| 681 | svc.proxySvc.InvalidateTarget(ctx, orphanDomain) | 695 | svc.proxySvc.InvalidateTarget(ctx, orphanDomain) | ||
| 682 | 696 | ||||