Port Allocation and Registry
Relevant source files
The following files were used as context for generating this wiki page:
The Port Allocation and Registry system is a core component of Docktree responsible for managing host port assignments across multiple Docker Compose worktrees. Its primary purpose is to prevent port collisions when running multiple instances of the same service stack simultaneously. By dynamically allocating ports from a configurable range and persisting these assignments in a global registry, Docktree ensures that every worktree project remains isolated and accessible on unique host addresses.
The system interacts with the Configuration to determine the allowed port ranges and uses a file-based locking mechanism to handle concurrent allocation requests from different worktrees safely.
Sources: README.md:1-15, internal/ports/registry.go:1-20
Architecture and Components
The registry is built around a file-based persistent store and a locking mechanism to ensure atomicity during allocation.
Registry Structure
The Registry struct manages the state of port assignments. It points to a global directory (usually ~/.config/docktree or defined by XDG_CONFIG_HOME) containing ports.json for data and ports.lock for concurrency control.
Data Models
The registry tracks assignments using the following data structures:
| Structure | Field | Type | Description |
|---|---|---|---|
| Assignment | Service | string | The name of the Docker Compose service. |
ContainerPort | int | The internal port defined in the container. | |
HostIP | string | The IP address to bind to (default: 127.0.0.1). | |
HostPort | int | The unique port allocated on the host machine. | |
| Range | Min | int | Start of the allocation range (default: 41000). |
Max | int | End of the allocation range (default: 49999). |
Sources: internal/ports/registry.go:13-16, internal/ports/registry.go:145-163, internal/config/config.go:132-137
Port Allocation Logic
The allocation process occurs during the docktree up command. The system attempts to find free ports within the specified range that are not currently recorded in the registry and are not actively bound by other processes on the host.
Allocation Workflow
- Request Identification: Docktree parses the Compose files to identify all services requiring published ports.
- Registry Locking: The system acquires an exclusive
syscall.Flockonports.lock. - Availability Check: It checks if the port is used in
ports.jsonand verifies actual network availability usingnet.Listen. - Assignment Persistence: New assignments are appended to the registry and saved to disk.
Conflict Resolution
In cases where a port is allocated in the registry but found to be bound by an external process during docker compose up, Docktree performs a retry logic. It will release the problematic assignment, re-lock the registry, and attempt a new allocation up to 10 times.
Sources: internal/cli/up.go:235-260, tests/concurrent_smoke_test.go:40-70
Configuration
Port allocation behavior is governed by the ports section in docktree.yml.
| Option | Type | Default | Description |
|---|---|---|---|
ports.range | string | "41000-49999" | The inclusive range of ports Docktree can use. |
ports.bind_host | string | "127.0.0.1" | The host interface for port binding. |
ports.mode | string | "dynamic" | Allocation strategy (currently defaults to dynamic). |
Sources: internal/config/config.go:126-140, internal/ports/registry.go:145-163
Registry Management
The CLI provides commands to inspect and manage the registry state across the entire system.
Viewing Assignments
Users can view port mappings for the current worktree or all managed instances.
docktree ports: Shows assignments for the active worktree.docktree ports --all: Aggregates assignments fromports.jsonfor all known instances.
Sources: internal/cli/status_ports.go:135-160, internal/cli/render.go:340-380
Resource Cleanup
The docktree clean command utilizes the registry to identify stale port allocations. If a worktree directory no longer exists on disk, its corresponding entries in ports.json are removed, freeing those ports for future instances.
Sources: internal/cli/render.go:480-520, internal/docker/cleanup.go:15-40
Implementation Details
The registry ensures data integrity through JSON serialization with Indent and atomic file writes.
// internal/ports/registry.go:77-85
func (r *Registry) Save(registry map[string][]Assignment) error {
if err := r.EnsureGlobalDir(); err != nil {
return err
}
data, err := json.MarshalIndent(registry, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(r.dir(), "ports.json"), append(data, '\n'), 0o644)
}
Sources: internal/ports/registry.go:77-85
The system also handles IPv4/IPv6 ambiguity by normalizing 127.0.0.1 and empty host strings during comparison to ensure consistency in the registry.
Sources: internal/ports/registry.go:210-215, internal/compose/override.go:167-170
Conclusion
The Port Allocation and Registry system provides the necessary coordination layer for Docktree's multi-worktree isolation. By combining persistent state, file-based locking, and active network probing, it provides a robust mechanism for managing thousands of potential service endpoints without manual intervention or port collision risks.