Zero-downtime port forwarding for running Docker containers.
Find a file
Joerg Ziefle 83a1179624 docs: add comprehensive documentation
- 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
2026-01-03 00:33:00 +01:00
src feat: initial implementation of porthole 2026-01-03 00:32:07 +01:00
tests test: add end-to-end integration tests 2026-01-03 00:32:34 +01:00
.gitignore feat: initial implementation of porthole 2026-01-03 00:32:07 +01:00
Cargo.toml feat: initial implementation of porthole 2026-01-03 00:32:07 +01:00
CLAUDE.md docs: add comprehensive documentation 2026-01-03 00:33:00 +01:00
PLAN.md docs: add comprehensive documentation 2026-01-03 00:33:00 +01:00
README.md docs: add comprehensive documentation 2026-01-03 00:33:00 +01:00

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 client
  • clap - CLI argument parsing
  • tokio - Async runtime
  • serde/serde_json - State persistence
  • anyhow/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 cleanup command
  • 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)
  • expose command (parse port spec, create proxy, save state)
  • unexpose command (remove proxy, update state)

Phase 2: List & Cleanup

  • list command (pretty table output, verify proxy status)
  • Manual cleanup command (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