Docker Compose

For projects using Docker Compose where multiple services need their own proxy ports, use the ports mapping with auto port assignment:

# mdp.yaml
services:
  frontend:
    command: npm run dev
    proxy: 3000

  infra:
    command: docker compose up
    log_split: compose     # split per-container logs into their own colored lanes
    env:
      API_PORT: auto       # mdp assigns a free port
      AUTH_PORT: auto
    ports:
      - env: API_PORT
        proxy: 4000        # registered as "<branch>/infra" on the 4000 proxy
      - env: AUTH_PORT
        proxy: 5000        # registered as "<branch>/infra" on the 5000 proxy

In your docker-compose.yml, reference the environment variables:

# docker-compose.yml
services:
  api:
    build: ./api
    ports:
      - "${API_PORT:-8080}:8080"
  auth:
    build: ./auth
    ports:
      - "${AUTH_PORT:-8081}:8080"

When you run mdp run, mdp assigns free ports, sets them as environment variables, and registers each port mapping with the appropriate proxy.

Per-container log splitting: log_split: compose parses compose's combined-stream output (<name> | <message>) and gives each container its own colored prefix. Lines that don't match (compose's own status output like Attaching to api-1, auth-1…) stay under the outer service's prefix. For ad-hoc commands outside mdp.yaml, pass --log-split=compose:

mdp run --log-split=compose -- docker compose up

For non-compose multiplexers (kubectl, honcho/foreman, bracket-prefixed tools), use the regex form — see log_split in the reference.

Non-HTTP ports (no proxy): omit proxy: on a ports: entry to allocate a free port for ${svc.env.VAR} interpolation without starting a reverse-proxy listener for it. Useful for databases, caches, and other non-HTTP services other services just need to connect to directly.

db:
  command: docker compose up db --wait
  env:
    DB_PORT: auto
  ports:
    - env: DB_PORT    # allocated & interpolatable, no proxy

UDP ports: add protocol: udp to a ports: entry so mdp allocates the host port with a UDP-aware free-port check and skips it in the depends_on readiness probe (TCP probes never succeed on UDP). This is what lets multiple worktrees run the same UDP-publishing compose stack in parallel — each gets its own random host port, and nothing collides.

# mdp.yaml
infra:
  command: docker compose up --wait
  env:
    JAEGER_AGENT_PORT: auto
  ports:
    - env: JAEGER_AGENT_PORT
      protocol: udp
# docker-compose.yml
services:
  jaeger:
    image: jaegertracing/all-in-one
    ports:
      - "${JAEGER_AGENT_PORT}:6831/udp"

UDP mappings are allocation-only: proxy: is rejected at config load.

HTTPS

mdp inherits TLS certificates from the services it proxies. When a service registers with --tls-cert and --tls-key, the proxy automatically starts accepting HTTPS connections using that certificate. Each proxy port serves both HTTP and HTTPS on the same port.

# Service provides its own cert — proxy inherits it
mdp run --tls-cert ./certs/localhost.pem --tls-key ./certs/localhost-key.pem -- npm run dev

# Auto-detect mkcert certs
mdp run --auto-tls -- npm run dev

Generate a local cert with mkcert:

mkcert localhost
mdp run --tls-cert localhost.pem --tls-key localhost-key.pem -- npm run dev

Stable ports

By default mdp remembers each branch's assigned ports and reuses them on the next mdp run, as long as the port is still free. Assignments are stored per repo + branch under ~/.mdp/ports/. This keeps ports steady across restarts — handy when a service binds a TLS cert (or a trust-store entry) to a specific port, so the one-time setup survives a restart instead of repeating every run.

The first run still picks ports at random, so different branches naturally get distinct ports. If a remembered port is taken by something else at startup, mdp transparently allocates a fresh one.

To turn it off, use --no-stable-ports — the universal off switch, valid for both mdp run (batch) and mdp run -- <command> (single-command) runs:

mdp run --no-stable-ports

For batch runs you can also disable it by default in mdp.yaml with stable_ports: false. (The config file isn't read for single-command runs, so use the flag there.)


← Back to docs index