Local development proxy for Docker — no more port conflicts.
Run multiple Docker projects simultaneously, each accessible via hostname:
http://myapp.localhosthttp://myapp-api.localhosthttp://myapp-admin.localhost
Running multiple Docker projects locally causes port conflicts:
Error: Bind for 0.0.0.0:6379 failed: port is already allocated
This happens because each project's docker-compose.yml maps the same host ports:
Project A: localhost:3000, localhost:5432, localhost:6379
Project B: localhost:3000, localhost:5432, localhost:6379 → CONFLICT ❌
dockroute runs Traefik as a reverse proxy. Web services are routed by hostname instead of port, and databases/caches stay internal to Docker's network — no exposed ports, no conflicts:
Project A: myapp.localhost → Traefik → container_a:3000
Project B: storefront.localhost → Traefik → container_b:3000
Databases and caches (Postgres, Redis, etc.) don't need ports: mappings at all — your app connects to them over Docker's internal network using the service name (e.g., redis:6379). No host port, no conflict.
Both projects run simultaneously with no conflicts.
brew tap designorant/tap
brew install dockroutegit clone https://github.com/designorant/dockroute.git
cd dockroute
./bin/dockroute startdockroute startdockroute statusOpen http://dockroute.localhost to see all routed services.
dockroute stopUpdate your project's docker-compose.yml:
- Web services: Replace
ports:with Traefik labels and add thedockroutenetwork - Databases/caches: Remove
ports:entirely — your app already connects via Docker's internal network
services:
app:
build: .
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.localhost`)"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"
networks:
- dockroute
- default
# Remove ports: - "3000:3000"
db:
image: postgres:16
# Remove ports: - "5432:5432"
# App connects as postgres://user:pass@db:5432/mydb
redis:
image: redis
# Remove ports: - "6379:6379"
# App connects as redis://redis:6379
networks:
dockroute:
external: trueThen start your project:
docker compose up -dAccess at: http://myapp.localhost
Run dockroute check in your project directory to verify your docker-compose.yml follows dockroute conventions:
cd ~/projects/myapp
dockroute checkIt checks for common issues — missing Traefik labels, exposed database ports, missing network declarations, generic router names, and nested subdomains — and prints a ready-to-paste suggestion block with the fixes.
Exits with code 0 when all checks pass, 1 when issues are found. You can pass an explicit path: dockroute check path/to/docker-compose.yml.
Each Docker Compose project has its own default network. Services like db and redis that stay on this network are automatically isolated — redis://redis:6379 in Project A connects to Project A's Redis, not Project B's, even with identical service names. No changes needed.
Services exposed through Traefik join the shared dockroute network where both router names and hostnames must be globally unique. Prefix them with your project name:
# Ambiguous — will collide with other projects using the same names
labels:
- "traefik.http.routers.app.rule=Host(`app.localhost`)"
- "traefik.http.services.app.loadbalancer.server.port=3000"
# Project-scoped — no conflicts
labels:
- "traefik.http.routers.myapp.rule=Host(`myapp.localhost`)"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"This also applies to supporting services like Mailhog or SonarQube — use myapp-mail.localhost instead of mail.localhost, and myapp-sonarqube.localhost instead of sonarqube.localhost.
WebSockets work over port 80 using hostname routing:
services:
soketi:
image: quay.io/soketi/soketi:1.6-16-debian
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp-ws.rule=Host(`myapp-ws.localhost`)"
- "traefik.http.services.myapp-ws.loadbalancer.server.port=6001"
networks:
- dockrouteAccess at: myapp-ws.localhost (no port needed)
Add to your project's mise.toml for automatic proxy startup:
[tasks.dev]
description = "Start development environment"
run = """
#!/bin/bash
dockroute ensure
docker compose up
"""
[tasks.dev-d]
description = "Start development environment (detached)"
run = """
#!/bin/bash
dockroute ensure
docker compose up -d
"""
[tasks.down]
description = "Stop development environment"
run = "docker compose down"Then use:
mise run dev # Start with logs
mise run dev-d # Start detached
mise run down # StopEverything routes through port 80 (or 443 with TLS) using hostnames — no port conflicts, no ports to remember:
| Service | Before | After | HTTPS |
|---|---|---|---|
| Web app | localhost:3000 |
http://myapp.localhost |
https://myapp.localhost |
| WebSockets | localhost:6001 |
http://myapp-ws.localhost |
https://myapp-ws.localhost |
| Mailhog | localhost:8025 |
http://myapp-mail.localhost |
https://myapp-mail.localhost |
| PostgreSQL | localhost:5432 |
myapp-db.localhost:5432 (TLS) |
— |
| Redis | localhost:6379 |
myapp-redis.localhost:6379 (TLS) |
— |
Databases and caches don't need exposed ports — your app connects via Docker's internal network using the service name (e.g., postgres://user:pass@db:5432/mydb).
For tools like Redis CLI or mysql, the simplest option is docker compose exec:
docker compose exec redis redis-cliFor GUI tools (Redis, MySQL) that require a host port, use an environment variable so each project can set its own port in .env:
# docker-compose.yml
services:
redis:
image: redis
ports:
- "${REDIS_PORT:-6379}:6379"# .env (gitignored, set once per project)
REDIS_PORT=6380The dashboard is available at dockroute.localhost.
Developers often run infrastructure (Postgres, Redis) in Docker but run application code natively for faster iteration. dockroute can route hostnames to native processes running on your machine — no Docker container needed:
# Route myapp.localhost to your local dev server on port 3000
dockroute route add myapp.localhost 3000
# Start your native app
cd ~/projects/myapp && npm run dev # listens on port 3000
# Access at http://myapp.localhost
curl http://myapp.localhostThis creates a Traefik file-provider route pointing at host.docker.internal:<port>, so it works alongside Docker-routed services. Use dockroute route for native apps and Docker labels for containerized apps.
dockroute route add myapp.localhost 3000 # Add a route
dockroute route add myapp.localhost 3000 --https # Add with HTTPS (requires tls setup)
dockroute route add myapp-db.localhost 5432 --tcp # Add TCP route (requires tls setup)
dockroute route add myapp.localhost 3000 --find-port # Auto-pick a free port (see below)
dockroute route list # List all host routes
dockroute route remove myapp.localhost # Remove a routeWhen your dev server's preferred port might be busy (another project, SSH tunnel, etc.), use --find-port to let dockroute pick the next available port automatically. The chosen port is printed to stdout so scripts can capture it:
# Auto-pick a free port starting from 3000
port=$(dockroute route add myapp.localhost 3000 --find-port)
npm run dev -- --port "$port"
# You only ever use the hostname — the port is invisible
curl http://myapp.localhostStatus output goes to stderr, so $() captures only the port number. If port 3000 is free, it uses 3000. If busy, it tries 3001, 3002, etc.
Route TCP traffic (e.g., PostgreSQL) to a native process on your machine — no Docker container needed:
# Route to a local Postgres server
dockroute route add myapp-db.localhost 5432 --tcp
# Connect via hostname
psql "host=myapp-db.localhost sslmode=require"TCP routes use Traefik's HostSNI() routing over dedicated entrypoints — postgres (port 5432) and redis (port 6379). The route port is the target port on your machine — often the same as the entrypoint port, but can differ if your local service listens elsewhere.
- Hostnames must be flat
<name>.localhost— nested subdomains (e.g.,mail.myapp.localhost) are not supported dockroute.localhostis reserved for the dashboard--httpsrequiresdockroute tls setupand creates dual-stack routing (HTTP + HTTPS)--tcprequiresdockroute tls setupand routes viaHostSNI()on the postgres entrypoint (port 5432)--httpsand--tcpare mutually exclusive- If a Docker container already claims the same hostname,
route addwill fail — remove the container's labels or choose a different hostname - Running
dockroute route addwith the same hostname replaces the existing entry (port, flags)
You can declare host routes in your docker-compose.yml using the x-dockroute extension. This lets dockroute check validate that the routes are registered:
x-dockroute:
routes:
- "myapp.localhost 3000"
- "myapp-api.localhost 3001 https"
- "myapp-db.localhost 5432 tcp"dockroute check will verify each entry's format (hostname, port, flags) and warn if a route isn't registered yet. It also detects conflicts between x-dockroute hostnames and Docker container labels in the same file.
After running dockroute tls setup, web services can use HTTPS via the websecure entrypoint. This is opt-in — HTTP-only services continue to work as before.
Add entrypoints=websecure and tls=true to your service labels:
services:
app:
build: .
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"
networks:
- dockroute
- default
networks:
dockroute:
external: trueAccess at: https://myapp.localhost
To redirect HTTP requests to HTTPS for a specific service, add the redirect-https@file middleware (provided by dockroute tls setup):
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp-http.rule=Host(`myapp.localhost`)"
- "traefik.http.routers.myapp-http.entrypoints=web"
- "traefik.http.routers.myapp-http.middlewares=redirect-https@file"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"This creates two routers: myapp handles HTTPS, and myapp-http catches HTTP requests and redirects them. No global redirect is applied — other services remain HTTP-only.
dockroute supports hostname-based TCP routing — multiple projects share the same port, each accessible via its own hostname:
psql "host=myapp-db.localhost sslmode=require" → Project A's Postgres
psql "host=storefront-db.localhost sslmode=require" → Project B's Postgres
redis-cli --tls -h myapp-redis.localhost → Project A's Redis
PostgreSQL uses STARTTLS — clients send a plain SSLRequest packet first, then upgrade to TLS. Traefik handles this natively: it recognizes the PostgreSQL negotiation, responds to it, then extracts the SNI hostname from the subsequent TLS handshake to route to the correct backend. This is why sslmode=require is mandatory — without it, the TLS handshake never happens and there's no hostname to route on.
Redis uses a dedicated TLS entrypoint on port 6379 where TLS starts immediately (no STARTTLS). Clients connect using the rediss:// scheme, and Traefik extracts the SNI hostname from the TLS handshake to route to the correct backend.
One-time setup (requires mkcert):
dockroute tls setup # Generates *.localhost TLS certs
dockroute stop && dockroute startAdd TCP labels to your PostgreSQL and/or Redis services:
services:
db:
image: postgres:16
labels:
- "traefik.enable=true"
- "traefik.docker.network=dockroute"
- "traefik.tcp.routers.myapp-db.rule=HostSNI(`myapp-db.localhost`)"
- "traefik.tcp.routers.myapp-db.entrypoints=postgres"
- "traefik.tcp.routers.myapp-db.tls=true"
- "traefik.tcp.services.myapp-db.loadbalancer.server.port=5432"
networks:
- dockroute
- default
redis:
image: redis:alpine
labels:
- "traefik.enable=true"
- "traefik.docker.network=dockroute"
- "traefik.tcp.routers.myapp-redis.rule=HostSNI(`myapp-redis.localhost`)"
- "traefik.tcp.routers.myapp-redis.entrypoints=redis"
- "traefik.tcp.routers.myapp-redis.tls=true"
- "traefik.tcp.services.myapp-redis.loadbalancer.server.port=6379"
networks:
- dockroute
- defaultNo ports: mapping needed — Traefik handles it.
Important: Services on multiple networks (both dockroute and default) must set traefik.docker.network=dockroute. Without it, Traefik may resolve the wrong network IP.
# PostgreSQL — psql
psql "host=myapp-db.localhost port=5432 sslmode=require user=postgres"
# PostgreSQL — ORMs
DATABASE_URL=postgres://user:pass@myapp-db.localhost:5432/mydb?sslmode=require
# Redis — redis-cli
redis-cli --tls -h myapp-redis.localhost -p 6379
# Redis — app connection string
REDIS_URL=rediss://myapp-redis.localhost:6379
# Internal app connections (unchanged, no TLS needed)
DATABASE_URL=postgres://user:pass@db:5432/mydb
REDIS_URL=redis://redis:6379Important: TLS is mandatory for host connections — sslmode=require for PostgreSQL, rediss:// for Redis. Without it, the TLS handshake never happens and there's no hostname to route on.
Node.js TLS trust: If using mkcert certificates, set NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" so Node.js trusts the local CA. For Redis via ioredis, you may also need to set tls.servername to the dockroute hostname for SNI matching.
- PostgreSQL and Redis only — MySQL's server-first protocol prevents SNI extraction
- Port conflicts — if a local PostgreSQL server (5432) or Redis server (6379) is running, stop it first or change its port
- Cert expiry — mkcert certs last ~27 months; check with
dockroute tls status
After switching to dockroute, your framework's dev server will still log its internal listen address:
▲ Next.js 16.1.1
- Local: http://localhost:3000
This is the address inside the container, not how you access the app. Use the dockroute hostname (myapp.localhost) instead. There's nothing to fix — every framework does this.
If your app sets a Content Security Policy, hardcoded localhost:PORT values will break. CSP directives like connect-src and script-src must match the dockroute hostnames:
- connect-src 'self' ws://localhost:6001
+ connect-src 'self' ws://myapp-ws.localhost:80For Next.js, read the host/port from environment variables instead of hardcoding:
// next.config.ts — build CSP from env vars
const soketiHost = process.env.NEXT_PUBLIC_SOKETI_HOST; // myapp-ws.localhost
const soketiPort = process.env.NEXT_PUBLIC_SOKETI_PORT; // 80
const connectSrc = soketiHost ? `ws://${soketiHost}:${soketiPort}` : '';docker-compose.yml defaults (via ${VAR:-default}) only apply when the variable is unset. If your .env file still has localhost:3000 values, they override the dockroute defaults. Update your .env to match:
NEXT_PUBLIC_APP_URL=http://myapp.localhost
NEXT_PUBLIC_SOKETI_HOST=myapp-ws.localhost
NEXT_PUBLIC_SOKETI_PORT=80Some frameworks use lazy-initialized module proxies to defer heavy setup (database connections, etc.) until first use. If a library checks "property" in proxy to branch its behavior, the in operator uses the proxy's has trap — not get. A proxy with only a get trap will check the raw target object instead, potentially taking the wrong code path. Add a has trap if your proxy wraps a lazily-created object:
export const instance = new Proxy({} as MyType, {
get(_, prop) { return Reflect.get(getRealInstance(), prop); },
has(_, prop) { return Reflect.has(getRealInstance(), prop); }, // ← don't forget this
});- Shared network: All projects connect to a
dockroutenetwork - Single proxy: Traefik listens on port 80 (dashboard at
dockroute.localhost) - Label-based routing: Traefik reads container labels to configure routes
- Hostname resolution:
.localhostdomains resolve to 127.0.0.1 automatically - Project isolation: Each project's
defaultnetwork keeps internal services separated; only Traefik-labeled services share thedockroutenetwork - File provider: Host routes (from
dockroute route) are written as Traefik file-provider configs, hot-reloaded without proxy restart
| Command | Description |
|---|---|
dockroute start |
Start the proxy |
dockroute stop |
Stop the proxy |
dockroute status |
Show status and routed services |
dockroute check [path] |
Check a compose file for dockroute issues |
dockroute logs |
Follow proxy logs |
dockroute ensure |
Start if not running (for scripts) |
dockroute route add <host> <port> [--https|--tcp] |
Route hostname to a local port |
dockroute route list |
List host routes |
dockroute route remove <host> |
Remove a host route |
dockroute tls setup |
Generate TLS certs for HTTPS, PostgreSQL, and Redis routing |
dockroute tls status |
Show TLS certificate status |
dockroute tls remove |
Remove TLS certs and config |
dockroute version |
Show version |
dockroute help |
Show help |
If you use AI coding agents (Claude Code, Cursor, Copilot, Codex, etc.), add the following to your project's AGENTS.md file so agents automatically follow dockroute conventions when working with your docker-compose.yml:
## Docker (dockroute)
This project uses [dockroute](https://github.com/designorant/dockroute) for local Docker networking. Services are accessible at `http://<prefix>.localhost`.
### Rules for `docker-compose.yml`
- **Web services**: Replace `ports:` with Traefik labels (`traefik.enable=true`, router rule, loadbalancer port). Add both `dockroute` and `default` networks. Use the project directory name as the `<prefix>`.
- **PostgreSQL**: Use TCP labels for hostname routing (`traefik.tcp.routers.<prefix>-db.rule=HostSNI(...)`, `entrypoints=postgres`, `tls=true`). Requires `dockroute tls setup`. Connect with `sslmode=require` from host.
- **Redis**: Use TCP labels for hostname routing (`traefik.tcp.routers.<prefix>-redis.rule=HostSNI(...)`, `entrypoints=redis`, `tls=true`). Requires `dockroute tls setup`. Connect with `rediss://` from host.
- **Internal services** (MySQL, caches): Remove `ports:` — apps connect via Docker's internal network. For host GUI access, use env var pattern: `"${MYSQL_PORT:-3306}:3306"`.
- **Supporting services** (Mailhog, MinIO, etc.): Route through Traefik with flat subdomain: `<prefix>-<service>.localhost` (NOT `<service>.<prefix>.localhost`).
- **WebSocket services**: Use `<prefix>-ws.localhost` as hostname.
- **Naming**: Router names and hostnames must be project-scoped (`myapp`, not `app`).
- **Network**: Declare `dockroute` as external.
- **Label format**: `"traefik.http.routers.<prefix>.rule=Host(`<prefix>.localhost`)"` (double-quoted, backtick hostnames).