- Rust 100%
- README with usage, features, testing guide - CLAUDE.md with architecture and file index - PLAN.md with implementation phases Documents 92 tests with clear breakdown: - 74 unit tests (port parsing, state management) - 13 Docker integration tests - 5 end-to-end tests validating actual traffic |
||
|---|---|---|
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| PLAN.md | ||
| README.md | ||
Porthole
Zero-downtime port forwarding for running Docker containers.
The Problem
Docker doesn't allow adding port mappings to running containers. The only option is to stop and recreate the container with new port configuration, causing 2-5 seconds of downtime.
For development workflows where you discover port needs during work (web servers, APIs, database ports), this disruption breaks flow.
The Solution
Porthole creates lightweight socat proxy containers that forward traffic without touching the target container:
# Expose port 8000 on running container
porthole expose mycontainer 8000
# Behind the scenes (simplified):
docker run -d \
--name porthole-proxy-mycontainer-8000 \
-p 127.0.0.1:8000:8000 \
alpine/socat tcp-listen:8000,fork,reuseaddr tcp:<target-ip>:8000
The proxy container runs on the bridge network and forwards traffic from its published port to the target container's IP address.
Platform Support
Works on all platforms: Linux, macOS, and Windows with Docker Desktop.
Usage
# Expose a port
porthole expose mycontainer 8000 # 8000:8000/tcp
porthole expose mycontainer 8080:3000 # Map host 8080 → container 3000
porthole expose mycontainer 5432/udp # UDP port
# List exposed ports
porthole list # All exposed ports
porthole list mycontainer # Ports for specific container
# Remove exposed port
porthole unexpose mycontainer 8000
# Cleanup orphaned proxies
porthole cleanup # Remove proxies for stopped containers
Features
- Zero downtime - Target container never restarts
- Lightweight - Each proxy is a tiny alpine/socat container
- Automatic cleanup - Removes proxies when target container stops
- Docker Compose support - Works with service names
- Persistent state - Survives porthole crashes
- Localhost only - All ports bind to 127.0.0.1 for security
Technical Approach
Language: Rust
Key Dependencies:
bollard- Async Docker API clientclap- CLI argument parsingtokio- Async runtimeserde/serde_json- State persistenceanyhow/thiserror- Error handling
State Management:
- Location:
~/.porthole/state.json - Tracks: container_id, host_port, container_port, protocol, proxy_container_id
- Atomic writes for consistency
Cleanup Strategy:
- Manual:
porthole cleanupcommand - Automatic: Monitor Docker Events API for container stops
Repository
https://git.mirus-tech.com/jmz/porthole.git
Implementation Phases
Phase 1: Core Functionality
- Docker API wrapper (connect, find container, create/remove proxy)
- State management (load/save JSON)
exposecommand (parse port spec, create proxy, save state)unexposecommand (remove proxy, update state)
Phase 2: List & Cleanup
listcommand (pretty table output, verify proxy status)- Manual
cleanupcommand (find and remove orphaned proxies) - Health checks (verify proxies running, detect conflicts)
Phase 3: Automatic Cleanup
- Docker Events API integration
- Automatic cleanup when containers stop
- Watch mode for continuous monitoring
Phase 4: Docker Compose Support
- Resolve service names to container IDs
- Handle compose labels (
com.docker.compose.service)
Testing
Porthole has comprehensive test coverage with 92 tests:
Unit Tests (74 tests)
- Port parsing: All formats, edge cases, property-based tests with proptest
- State management: Atomic writes, JSON serialization, concurrent operations
- Coverage of all core business logic
Integration Tests (13 tests)
- Docker API operations (container discovery, inspection, lifecycle)
- Proxy container creation and removal
- Port conflict detection
End-to-End Tests (5 tests)
- HTTP traffic validation: Creates nginx container, exposes port, makes actual HTTP request
- Multiple port mappings
- State persistence
- Port conflict prevention
- Container restart resilience
# Run all tests
cargo test
# Run specific test suites
cargo test --lib # Unit tests only
cargo test --test docker_integration # Docker integration tests
cargo test --test e2e_expose # End-to-end tests
# Run e2e tests serially (recommended to avoid resource contention)
cargo test --test e2e_expose -- --test-threads=1
Note: Integration and e2e tests require Docker to be running. They will skip gracefully if Docker is unavailable.
Why Rust?
- Performance: Minimal overhead for a tool that might run as daemon
- Type safety: Prevents common bugs in state management
- Async: Natural fit for Docker API operations
- Tooling: Excellent CLI ecosystem (clap, tokio)
- Error handling: Robust error types with thiserror/anyhow
License
MIT