Docker Compose with auto-routing. HTTP services get *.localhost URLs via Traefik, TCP services (databases, etc.) get
deterministic port mappings. Works great with git worktrees.
When running multiple Docker Compose projects (or the same project in multiple worktrees), port conflicts are inevitable. You end up manually juggling port numbers or stopping one project to start another.
tug uptug wraps Docker Compose and automatically:
- Routes HTTP services through Traefik at
http://<service>.<project>.localhost - Assigns deterministic host ports to TCP services (PostgreSQL, Redis, etc.) using a hash of the project + service name — no more conflicts
- Passes through any other command to
docker composeas-is
Two projects, same ports, zero conflicts:
~/examples/app-a $ tug ps
SERVICE TYPE URL/PORT STATUS
api http http://api.app-a.localhost running
web http http://web.app-a.localhost running
postgres tcp localhost:19315 → 5432 running
redis tcp localhost:29042 → 6379 running
~/examples/app-b $ tug ps
SERVICE TYPE URL/PORT STATUS
api http http://api.app-b.localhost running
postgres tcp localhost:54817 → 5432 running
redis tcp localhost:38291 → 6379 running
brew install mickamy/tap/tugGrab the latest release from GitHub Releases and place it in your $PATH.
go install github.com/mickamy/tug@latestmake install- Docker (or Podman) with Compose v2.24+
- Port 80 available for Traefik
tug up # start services with auto-routing
tug down # stop services and clean up
tug ps # show URLs and port mappings
tug ps --json # machine-readable output
tug prune # stop Traefik and remove tug network
tug logs -f api # passthrough to docker compose
tug exec db psql # passthrough to docker compose-f, --file Specify compose file (default: auto-detect)
--override Layer an additional override file (repeatable)
--version, -v Print version
-h, --help Show help
Layer additional compose files on top of the base, just like docker compose -f:
tug --override staging.yaml up
tug --override a.yaml --override b.yaml uptug merges them via docker compose config, then generates its own routing override on top.
Create a .tug.yaml in your project root (or ~/.config/tug.yaml for global defaults). Project-local config takes
priority.
command:
compose: "podman compose" # default: "docker compose"
runtime: "podman" # default: "docker"By default, tug detects TCP services by well-known ports (5432, 3306, 6379, etc.) and treats everything else as HTTP. Override this per service:
services:
my-grpc-server:
kind: tcpFor services that expose both HTTP and TCP ports (e.g., a SQL proxy with a web UI and a gRPC port):
services:
sql-tap:
ports:
8081: http # web UI → Traefik
9091: tcp # gRPC → deterministic portPriority: per-port config > per-service kind > well-known port detection > HTTP default.
- Parse the compose file (and any
--overridefiles) - Classify each port as HTTP or TCP
- Generate
.tug/override.yamlwith Traefik labels and port remappings - Start Traefik if not already running (shared across all projects)
- Run
docker compose -f <base> -f .tug/override.yaml up
The generated .tug/ directory is in .gitignore — it's ephemeral.
TCP ports are mapped using FNV-1a hash of project + service + container port, landing in the 10000–60000 range. The
same project always gets the same ports, even across machines.
tug runs a single shared Traefik instance (tug-traefik container) that routes all HTTP services. It's created on first
tug up and persists across projects. tug down does not stop Traefik since other projects may be using it.