| ← Back to README | Configuration | Deployment → |
Related: Specification Generator
This document describes pactown’s dynamic port allocation and service discovery system.
When running multiple services:
Pactown uses dynamic port allocation and name-based service discovery:
services:
api:
depends_on:
- name: database # ← resolved to actual URL at runtime
Services communicate via names, not ports. The actual port is injected via environment variables.
When starting a service, pactown:
port firstPort 8003 busy, using 10042Source: network.py:PortAllocator
class PortAllocator:
def allocate(self, preferred_port: Optional[int] = None) -> int:
# Try preferred port first
if preferred_port and self.is_port_free(preferred_port):
return preferred_port
# Find next available
for port in range(self.start_port, self.end_port):
if self.is_port_free(port):
return port
The ServiceRegistry tracks running services and their endpoints:
Source: network.py:ServiceRegistry
registry = ServiceRegistry()
# Register service with dynamic port
endpoint = registry.register("api", preferred_port=8001)
print(endpoint.url) # http://127.0.0.1:10001 (if 8001 was busy)
# Get dependency URLs
env = registry.get_environment("web", ["api", "database"])
# {
# "API_URL": "http://127.0.0.1:10001",
# "API_HOST": "127.0.0.1",
# "API_PORT": "10001",
# "DATABASE_URL": "http://127.0.0.1:10002",
# ...
# }
Each service receives environment variables for its dependencies:
| Variable | Example | Description |
|---|---|---|
{SERVICE}_URL |
DATABASE_URL=http://127.0.0.1:10002 |
Full URL |
{SERVICE}_HOST |
DATABASE_HOST=127.0.0.1 |
Host only |
{SERVICE}_PORT |
DATABASE_PORT=10002 |
Port only |
MARKPACT_PORT |
MARKPACT_PORT=10001 |
Own port |
SERVICE_NAME |
SERVICE_NAME=api |
Service name |
Dynamic ports are enabled by default. To use fixed ports only:
orchestrator = Orchestrator(config, dynamic_ports=False)
Or via API:
from pactown import Orchestrator
orch = Orchestrator.from_file("saas.pactown.yaml", dynamic_ports=True)
Default range: 10000-65000
Customize in code:
from pactown.network import PortAllocator
allocator = PortAllocator(start_port=20000, end_port=30000)
The service registry persists to .pactown-sandboxes/.pactown-services.json:
{
"services": {
"database": {
"name": "database",
"host": "127.0.0.1",
"port": 10000,
"health_check": "/health"
},
"api": {
"name": "api",
"host": "127.0.0.1",
"port": 10001,
"health_check": "/health"
}
}
}
This allows services to find each other even after restarts.
ServiceEndpoint@dataclass
class ServiceEndpoint:
name: str # Service name
host: str # Host address
port: int # Allocated port
health_check: str # Health endpoint
@property
def url(self) -> str: ... # http://host:port
@property
def health_url(self) -> str: ... # http://host:port/health
ServiceRegistryclass ServiceRegistry:
def register(name, preferred_port, health_check) -> ServiceEndpoint
def unregister(name) -> None
def get(name) -> Optional[ServiceEndpoint]
def get_url(name) -> Optional[str]
def list_services() -> list[ServiceEndpoint]
def get_environment(service_name, dependencies) -> dict[str, str]
def clear() -> None
PortAllocatorclass PortAllocator:
def is_port_free(port) -> bool
def allocate(preferred_port=None) -> int
def release(port) -> None
def release_all() -> None
from pactown.network import find_free_port, check_port
# Find any free port
port = find_free_port()
# Check if specific port is available
if check_port(8080):
print("Port 8080 is free")
For full network isolation with DNS-style hostnames:
# Future feature
network:
mode: docker
name: pactown-net
Services would be accessible as database.pactown.local, api.pactown.local, etc.