@jdtzmn/port
v0.1.3
Published
CLI tool for managing git worktrees with automatic Traefik configuration
Readme
Port
Run 2+ Docker compose worktrees on the same service ports at the same time without conflicts.
Features
- Git Worktree Management: Create and manage git worktrees with a single command
- Automatic Traefik Configuration: Dynamically configure Traefik reverse proxy for local domain access
- Port Conflict Resolution: Run multiple worktrees simultaneously without port conflicts
- Host Process Support: Run non-Docker processes (like
npm serve) with Traefik routing - DNS Setup: Automated DNS configuration for
*.portdomains - Service Discovery: Easy access to services via hostnames instead of port numbers
Installation
# Port is published on npm, but requires Bun at runtime
npm install -g @jdtzmn/port
# or install globally with Bun
bun add -g @jdtzmn/portport executes with a Bun shebang (#!/usr/bin/env bun), so Bun must be installed and available on PATH even when the package is installed via npm.
Quick Start
Want a guided workflow in the CLI?
port onboard1. Initialize Project
port initThis sets up the .port/ directory structure and checks DNS configuration.
2. Configure Project
Create .port/config.jsonc in your project:
{
// Optional, defaults to "port"
"domain": "port",
// Optional, defaults to "docker-compose.yml"
"compose": "docker-compose.yml",
}3. Set Up DNS (One-time)
port installConfigures your system to resolve your configured wildcard domain (default *.port) to 127.0.0.1.
On macOS, port install runs privileged steps through a centralized elevation helper: it uses the native admin credential dialog when a GUI session is available and falls back to terminal sudo in headless/non-GUI environments.
You can optionally specify a custom IP address:
# Resolve to a specific IP (useful for Docker networks, etc.)
port install --dns-ip 172.25.0.2
# Skip confirmation prompt
port install --yes
# Combine options
port install --yes --dns-ip 192.168.1.100
# Explicit custom domain
port install --domain customLinux DNS Setup
On Linux systems with systemd-resolved running (most modern Ubuntu/Debian systems), the install command automatically:
- Detects that
systemd-resolvedis using port 53 - Runs
dnsmasqon port 5354 to avoid conflicts - Configures
systemd-resolvedto forward your wildcard domain queries to dnsmasq
This "dual-mode" setup allows both services to coexist without conflicts.
4. Shell Integration (Recommended)
Add this to your shell profile so port enter and port exit can change your working directory:
# ~/.bashrc
eval "$(port shell-hook bash)"
# ~/.zshrc
eval "$(port shell-hook zsh)"
# ~/.config/fish/config.fish
port shell-hook fish | sourceWithout shell integration, port enter and port exit will print a cd command for you to run manually.
5. Enter a Worktree
port feature-1
port enter feature-1This creates a new worktree and changes into it (with shell integration) or prints the path to cd into.
Use port enter <branch> when your branch name collides with a command (for example status or install).
If a branch and command collide, running port <command> shows a hint to use port enter <branch>.
6. Exit a Worktree
port exitReturns to the repository root and clears the PORT_WORKTREE environment variable.
7. Start Services
port upStarts docker-compose services and makes them available at feature-1.port:PORT.
8. Stop Services
port downStops services and optionally shuts down Traefik if no other projects are running.
9. Run Host Processes (Non-Docker)
port run 3000 -- npm run devRuns a host process (not in Docker) and routes traffic through Traefik. The command receives the PORT environment variable set to an ephemeral port, while users access it via <branch>.port:3000.
This is useful for:
- Development servers that don't run in Docker
- Quick testing without containerization
- Running multiple instances of the same service on different worktrees
10. List Active Worktrees
port listShows a concise worktree-level running/stopped summary and any running host services.
For per-service details by worktree:
port statusShow URLs for services in the current worktree:
port urls
port urls ui-frontendport urls works in either a worktree or the main repository.
11. Remove a Worktree
port remove feature-1
# Skip confirmation for non-standard/stale worktree entries
port rm -f feature-1
# Keep the local branch name unchanged
port rm --keep-branch feature-1Stops services, removes the worktree, and soft-deletes the local branch by archiving it under archive/<name>-<timestamp>.
Use --keep-branch to preserve the local branch name.
12. Clean Up Archived Branches
port cleanupShows archived branches created by port remove and asks for confirmation before deleting all of them.
Commands
| Command | Description |
| ------------------------------------------------ | ----------------------------------------------------- |
| port init | Initialize .port/ directory structure |
| port onboard | Print recommended workflow and command usage guide |
| port install [--dns-ip IP] [--domain DOMAIN] | Set up DNS for wildcard domain (default from config) |
| port shell-hook <bash\|zsh\|fish> | Print shell integration code for automatic cd |
| port enter <branch> | Enter a worktree explicitly (including command names) |
| port <branch> | Enter a worktree (creates if doesn't exist) |
| port exit | Exit the current worktree and return to repo root |
| port up | Start docker-compose services in current worktree |
| port down | Stop docker-compose services and host processes |
| port run <port> -- <command...> | Run a host process with Traefik routing |
| port kill [port] | Stop host services (optionally by logical port) |
| port remove <branch> [--force] [--keep-branch] | Remove worktree and archive local branch |
| port compose <args...> | Run docker compose with auto -f flags |
| port list | List worktree and host-service summary |
| port status | Show per-service status by worktree |
| port urls [service] | Show service URLs for current worktree |
| port cleanup | Delete archived local branches with confirmation |
| port uninstall [--yes] [--domain DOMAIN] | Remove DNS configuration for wildcard domain |
How It Works
Architecture
┌─────────────────────────────────┐
│ CLI Tool: port │
│ (installed globally) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Your Project: ~/projects/my-app │
│ ├── .port/ │
│ │ ├── config.jsonc │
│ │ └── trees/ │
│ │ ├── feature-1/ │
│ │ └── feature-2/ │
│ └── docker-compose.yml │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Traefik (global) │
│ ~/.port/traefik/ │
│ - Routes by hostname │
│ - Manages all services │
└─────────────────────────────────┘Port Conflict Resolution
Multiple worktrees can run simultaneously because:
- Host port bindings are disabled in worktree overrides
- Services only listen on internal container ports
- Traefik routes by Host header, not port number
Example:
# feature-1 worktree
port feature-1
port up
# Available at: feature-1.port:3000
# In another terminal, feature-2 worktree (same ports!)
port feature-2
port up
# Available at: feature-2.port:3000
# No conflicts! Traefik routes both to the same internal port on different containersHost Process Routing
The port run command enables running non-Docker processes with Traefik routing:
# In .port/trees/feature-1 directory
port run 3000 -- npm run dev
# Service available at http://feature-1.port:3000
# In another terminal, .port/trees/feature-2 directory
port run 3000 -- npm run dev
# Service available at http://feature-2.port:3000
# No port conflicts! Both run simultaneously.How it works:
- Allocates a unique ephemeral port (e.g., 49152)
- Sets
PORT=49152environment variable for the command - Registers with Traefik:
feature-1.port:3000→localhost:49152 - Cleans up when the process exits (Ctrl+C, crash, etc.)
Most frameworks (Express, Next.js, Vite, etc.) respect the PORT environment variable automatically.
Project Structure
port/
├── package.json
├── tsconfig.json
├── eslint.config.ts
├── prettier.config.js
├── src/
│ ├── index.ts # Entry point
│ ├── commands/
│ │ ├── init.ts
│ │ ├── install.ts
│ │ ├── enter.ts
│ │ ├── exit.ts
│ │ ├── shell-hook.ts # Shell integration (bash/zsh/fish)
│ │ ├── up.ts
│ │ ├── down.ts
│ │ ├── run.ts # Host process runner
│ │ ├── remove.ts
│ │ ├── list.ts
│ │ └── status.ts
│ ├── lib/
│ │ ├── config.ts
│ │ ├── git.ts
│ │ ├── compose.ts
│ │ ├── shell.ts # Shell command generation
│ │ ├── traefik.ts
│ │ ├── registry.ts
│ │ ├── hostService.ts # Host service management
│ │ ├── dns.ts
│ │ ├── sanitize.ts
│ │ └── worktree.ts
│ └── types.ts
├── traefik/
│ └── docker-compose.yml
└── README.mdRequirements
- Bun 1.0+ (required runtime for the
portCLI) - Git 2.7+
- Docker & Docker Compose v2.24.0+
- macOS or Linux
Configuration
See PLAN.md for detailed configuration options and examples.
Development
# Install dependencies
bun install
# Run in development
bun run dev init
# Build
bun run build
# Type check
bun run typecheck
# Format code
bun run format
# Lint
bun run lint
# Test
bun run testTesting in Ubuntu Container
The project includes a Docker container running Ubuntu 24.04 with systemd for testing the CLI in a Linux environment. This is useful for testing DNS configuration and other Linux-specific features.
# Start the container and open a bash shell
make ubuntu
# Stop the container
make downOnce inside the container, you can test the CLI:
# Set up DNS for *.port domains
port install --yes
# Test DNS resolution
dig test.portNote: The container overrides
/etc/resolv.confto use systemd-resolved for DNS, which allows*.portdomain resolution to work. However, this means the container does not have access to the outside network (e.g.,apt-get updateorcurlto external URLs will fail).
Compose Overrides Reference
Port isolates worktrees in layered compose files:
- Your base compose file (
docker-compose.ymlby default) - A generated Port override (
.port/override.yml) - An optional rendered user override (
.port/override.user.yml)
Port runs compose with user overrides last so local customization wins:
docker compose -p <project-name> -f docker-compose.yml -f .port/override.yml -f .port/override.user.yml up -d.port/override.user.yml is generated at runtime from .port/override-compose.yml if that file exists.
Here are all Port-managed overrides/compose controls and why they exist:
| Port-managed change | Why it is necessary |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| -p <project-name> (compose flag) | Namespaces compose resources per repo/worktree so similarly named stacks do not collide. |
| -f .port/override.yml (compose flag) | Applies Port's deterministic runtime adjustments without mutating your source compose file. |
| -f .port/override.user.yml (compose flag, optional) | Applies user-provided overrides rendered from .port/override-compose.yml, after Port defaults, so user rules win. |
| services.<name>.ports: !override [] (for services with published ports) | Removes host port binds so two worktrees can both run services that declare the same host ports. |
| services.<name>.labels: [...] | Adds Traefik router/service metadata so requests route by hostname (<branch>.port) instead of host port ownership. |
| services.<name>.networks: [traefik-network] | Ensures Traefik can reach exposed services on the shared network. |
| services.<name>.container_name rewrite (only when upstream sets one) | Prevents global Docker container name conflicts when upstream hard-codes a fixed container_name. |
| networks.traefik-network.external: true | Connects project services to the globally managed Traefik network instead of creating per-project duplicates. |
Notes:
- For services without published ports, Port does not inject Traefik labels/ports/network wiring.
- Port intentionally does not override
image,build,environment,volumes,depends_on, orcommand. .port/override-compose.ymlis optional and user-editable; if missing, Port skips the user layer.- Supported user override variables:
PORT_ROOT_PATH,PORT_WORKTREE_PATH,PORT_BRANCH,PORT_DOMAIN,PORT_PROJECT_NAME,PORT_COMPOSE_FILE.
Example generated shape:
services:
web:
container_name: my-repo-feature-1-web
ports: !override []
networks:
- traefik-network
labels:
- traefik.enable=true
- traefik.http.routers.feature-1-web-3000.rule=Host(`feature-1.port`)
- traefik.http.routers.feature-1-web-3000.entrypoints=port3000
- traefik.http.routers.feature-1-web-3000.service=feature-1-web-3000
- traefik.http.services.feature-1-web-3000.loadbalancer.server.port=3000
networks:
traefik-network:
external: true
name: traefik-networkLicense
MIT
