Host Port Management
Planning: HostPortManager — Dev Port Collision Detection and Auto-Assignment
Section titled “Planning: HostPortManager — Dev Port Collision Detection and Auto-Assignment”Context
Section titled “Context”Multiple project templates default to the same host port (8080), so users who run more than one project locally will have silent port collisions. This adds HostPortManager to:
- Detect and warn about existing port conflicts during
local build - Auto-assign non-colliding ports when
dapsman initcopies a template
No YAML parsing library is needed — the port binding format ("127.0.0.1:<host>:<container>") is consistent across all dev compose files and extractable with a simple regex.
New Types
Section titled “New Types”Application layer — src/Dapsman.Application/
Section titled “Application layer — src/Dapsman.Application/”HostPortBinding.cs
public sealed record HostPortBinding( string ProjectName, string FilePath, int HostPort, int ContainerPort);HostPortConflict.cs
public sealed record HostPortConflict( int Port, IReadOnlyList<HostPortBinding> Bindings);In Interfaces.cs — add:
public interface IHostPortManager{ IReadOnlyList<HostPortBinding> GetBindings(string projectName, IEnumerable<string> devComposeFilePaths); IReadOnlyList<HostPortConflict> FindConflicts(IReadOnlyList<HostPortBinding> bindings); int FindNextAvailable(int preferredPort, IReadOnlyCollection<int> reservedPorts);}Domain layer — src/Dapsman.Domain/
Section titled “Domain layer — src/Dapsman.Domain/”PortAssignment.cs — lives in Domain (not Application) so InitPlan can reference it without a circular dependency:
public sealed record PortAssignment(int OriginalPort, int AssignedPort, string Reason);Infrastructure layer — src/Dapsman.Infrastructure/HostPortManager.cs
Section titled “Infrastructure layer — src/Dapsman.Infrastructure/HostPortManager.cs”GetBindings(): reads each file, applies regex127\.0\.0\.1:(\d+):(\d+)to find host:container port pairs, returnsHostPortBindinglist tagged with project name and file pathFindConflicts(): groups bindings by host port, returns groups with count > 1FindNextAvailable(): increments frompreferredPortuntil finding a port not inreservedPortsand not in a built-in skip list (ports < 1024, and common service ports: 3306, 5432, 6379, 27017, 5672, 8161, 5900)
Note: Reserved ports only gate FindNextAvailable — projects may still explicitly bind reserved ports (e.g. 3306 for a local MySQL service). GetBindings and FindConflicts treat all ports equally.
local build — Collision Warnings
Section titled “local build — Collision Warnings”ConventionComposePlanBuilder
Section titled “ConventionComposePlanBuilder”Takes IHostPortManager as a constructor parameter. Resolves all configured projects (not just the --project filter) to collect LocalDevComposeFiles for port conflict detection — so a filtered local build still reports collisions involving projects that weren’t selected. Calls GetBindings() for each project, then FindConflicts(). Appends each conflict as a warning:
warning: Port 8080 is used by multiple projects: dapster-wp (compose_dapster-wp.dev.yaml), loccom (compose_loccom.dev.yaml)LocalDevComposeFiles is a property on ProjectDockerDefinition that returns only the .dev.yaml project compose files (excluding the shared base and daps extension files). This replaces an earlier inline .Where(f => f.EndsWith(".dev.yaml", ...)) filter.
Warnings flow into LocalBuildPlan.Warnings, already printed by DapsmanPlanPrinter.PrintLocalBuild.
Wiring in DapsmanRunner
Section titled “Wiring in DapsmanRunner”new HostPortManager() passed into ConventionComposePlanBuilder in RunLocalBuild().
dapsman init — Auto Port Assignment
Section titled “dapsman init — Auto Port Assignment”InitPlan (Domain)
Section titled “InitPlan (Domain)”public IReadOnlyList<PortAssignment> DevPortAssignments { get; init; } = [];ConventionInitPlanBuilder
Section titled “ConventionInitPlanBuilder”Takes IHostPortManager, IProjectResolver, and IDockerResolver as constructor parameters. At plan time:
- Reads template’s
_docker/*.dev.yamlto extract desired host ports - Collects all existing configured projects’ bindings via
LocalDevComposeFiles(dev-only files, fromProjectDockerDefinition) - For each template port: if it conflicts, calls
FindNextAvailable()passing all already-reserved + already-assigned ports - Populates
DevPortAssignmentson the plan
Preferred-port-with-fallback: templates define natural defaults (e.g. 8080 for WordPress); init only changes a port if there’s a collision.
InitService.Execute()
Section titled “InitService.Execute()”After CopyTemplateDirectory(), applies port assignments: for each PortAssignment where AssignedPort != OriginalPort, scans _docker/*.dev.yaml in the destination and replaces 127.0.0.1:{OriginalPort}: → 127.0.0.1:{AssignedPort}:. Follows the same pattern as ApplyProdUrl().
DapsmanPlanPrinter.PrintInit()
Section titled “DapsmanPlanPrinter.PrintInit()”- dev port assignments: 8080 → 8083 (conflict with dapster-wp) 8082 (available) 3306 (available)Wiring in DapsmanRunner
Section titled “Wiring in DapsmanRunner”new HostPortManager() passed to ConventionInitPlanBuilder in RunInit().
Files Changed
Section titled “Files Changed”| File | Change |
|---|---|
src/Dapsman.Application/Interfaces.cs | Add IHostPortManager |
src/Dapsman.Application/HostPortBinding.cs | New record |
src/Dapsman.Application/HostPortConflict.cs | New record |
src/Dapsman.Domain/PortAssignment.cs | New record (Domain, not Application) |
src/Dapsman.Domain/InitPlan.cs | Add DevPortAssignments |
src/Dapsman.Infrastructure/HostPortManager.cs | New class |
src/Dapsman.Domain/ProjectDockerDefinition.cs | Add LocalDevComposeFiles property (dev-only compose files, excludes shared base and daps extension) |
src/Dapsman.Infrastructure/DockerResolver.cs | Populate LocalDevComposeFiles by filtering localComposeFiles to .dev.yaml entries |
src/Dapsman.Infrastructure/ConventionComposePlanBuilder.cs | Add IHostPortManager; scan all projects (not just selected) for conflict detection using LocalDevComposeFiles; always include all projects’ toolkit mount files |
src/Dapsman.Infrastructure/ConventionInitPlanBuilder.cs | Add IHostPortManager + resolvers; use LocalDevComposeFiles for existing project bindings |
src/Dapsman.Application/InitService.cs | ApplyDevPorts() in Execute() |
src/Dapsman.Cli/DapsmanPlanPrinter.cs | Show port assignments in PrintInit() |
src/Dapsman.Cli/DapsmanRunner.cs | Wire HostPortManager into RunInit() and RunLocalBuild() |
tests/Dapsman.Infrastructure.Tests/HostPortManagerTests.cs | New test class |
tests/Dapsman.Infrastructure.Tests/DockerResolver_IntegrationTests.cs | New integration tests confirming LocalProjectComposeFiles includes shared file and LocalDevComposeFiles is dev-only |
tests/Dapsman.Infrastructure.Tests/Fakes.cs | Add FakeHostPortManager |
tests/Dapsman.Infrastructure.Tests/ConventionComposePlanBuilderTests.cs | Pass FakeHostPortManager |
tests/Dapsman.Infrastructure.Tests/ConventionInitPlanBuilderTests.cs | Pass resolvers + FakeHostPortManager |
Unit Tests — HostPortManagerTests
Section titled “Unit Tests — HostPortManagerTests”GetBindings_ExtractsPortsFromDevComposeFileGetBindings_MultipleServicesInOneFile— WordPress dev compose has 3 port bindingsGetBindings_ReservedPortAllowed— port 3306 in a compose file is returned normallyGetBindings_IgnoresNonLocalBindings—"8080:80"(no IP) is not matchedGetBindings_MissingFileReturnsEmptyFindConflicts_NoConflicts_ReturnsEmptyFindConflicts_DetectsCollisionFindNextAvailable_ReturnsPreferredIfFreeFindNextAvailable_SkipsReservedPortsFindNextAvailable_SkipsWellKnownPorts— ports < 1024 always skippedFindNextAvailable_SkipsCommonServicePorts— e.g. 3306 skipped by built-in list