Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

unport gives every Docker Compose service a real HTTPS hostname like api.shop.test instead of a localhost port

MIT license Node 24 or newer Docker Compose v2 macOS and Linux

Run your Docker Compose projects at real hostnames like https://api.shop.test,
without publishing host ports or changing how you start anything.

The problem

You run more than one Compose project on the same machine. Each one publishes a handful of ports, so your .env files fill up with localhost:8080, localhost:5433, localhost:3000, and every so often two projects both want 3000 and one of them loses. You end up keeping a mental map of which number is which service, and it falls out of your head the moment you switch projects.

unport gives each web service a hostname instead. api.shop.test, web.blog.test, over local HTTPS, resolving to your own machine. Databases and other TCP services keep a loopback port, but a stable one that unport tracks for you. You do not edit your compose file, and you do not learn a new way to start things. unport link writes one generated override, and your usual docker compose up picks it up.

One honest detail up front: this is not zero ports on your machine. It is two ports, 80 and 443, held by a single small proxy container and shared by every project. That is the whole trick.

Quick start

npm i -g unport-cli

# once per machine: local CA, DNS resolver, shared network, edge
unport init

Then, inside any Docker Compose project:

unport link
docker compose up -d

unport init asks for sudo once, to install a DNS resolver file and trust the local certificate authority. It prints exactly what it is about to change before it changes it, and unport uninstall reverses all of it.

If you would rather look before touching anything, unport plan prints what unport would do to the current project and changes nothing:

$ unport plan
unport plan · shop
tld .test   bind 127.0.0.1   edge :80 :443   5 services

HTTP ROUTES (1)
  api          :8000   → https://api.shop.test

TCP FORWARDS (2)
  db           :5432   → 127.0.0.1:29817 moved (postgresql)
  cache        :6379   → 127.0.0.1:6379  kept  (redis)

INTERNAL (2) — no published ports, untouched

NOTES
  ! db: host port 5433 is already in use — moved to 29817

What you type

unport writes files and answers questions. It does not run your project; that stays docker compose.

Set up once per machine:

Command What it does
unport init Install the local CA, DNS resolver, shared network, and edge. Reversible.
unport uninstall Undo everything init did.
unport doctor Diagnose the install and the machine. The first thing to run when a page will not load.
unport ca Inspect or trust the local certificate authority: path, trust, untrust.
unport edge Manage the proxy container: start, stop, status.

Day to day:

Command What it does
unport link / unport unlink Write or remove the compose override for the current project.
unport ls Every routed service on the machine, across all projects, with health and address.
unport url [svc] Print one address, for scripts: curl $(unport url api)/health.
unport open [svc] Open a service in the browser, after checking it will actually load.
unport env [svc] Print connection variables for host tooling, such as DATABASE_URL.
unport plan Show what unport would do to this project. Read-only.

When you need to step outside the happy path:

Command What it does
unport connect <svc> Forward a database or other TCP service to a local port.
unport eject Write the override as a real compose file you own, and leave unport behind.

How it works

A request to api.shop.test resolves through the DNS resolver to 127.0.0.1, reaches the one edge container holding ports 80 and 443, and is routed by hostname over the shared unport network to the api container

Three parts, all set up once by unport init:

  1. A DNS resolver, so every *.test name points at loopback on your host.
  2. One long-lived proxy container (Traefik) on a shared unport network. It holds the only published ports on the machine, 80 and 443.
  3. A generated compose override. unport link writes it: strip each service's ports:, attach the unport network, add routing labels. Your docker compose up reads it through COMPOSE_FILE.

The edge has to be a container, not a process on your host. On macOS the host has no route to container IPs, because containers live inside a VM behind NAT, so a host-side proxy could not reach 172.18.0.4:3000 even if it tried. Running the proxy inside Docker, on the shared network, is what makes hostname routing work at all.

Databases and other TCP services

Postgres, MySQL, Redis and the like cannot be routed by hostname. There is no Host header to read, and they do not begin the connection in TLS, so the edge has nothing to switch on. unport handles them differently: it keeps the port your compose file asked for and publishes it on loopback. If another project already holds that port, unport moves the service to a free one and remembers the number.

When a port gets moved, the connection string in your .env or your saved GUI still points at the old one. unport connect restores it on demand:

unport connect db              # forward the port you asked for to wherever it lives now
unport connect db --port 6000  # or pin a local port yourself

It holds a plain socket forward open until you press Ctrl-C. There is no extra container involved, because the service is already on loopback.

Leaving unport

unport should never be a one-way door. unport eject writes the generated override into your repo as a real compose file you own, removes the hidden one, and prints the plain command to run it:

$ unport eject
  + compose.unport.yml
  run: docker compose -f docker-compose.yml -f compose.unport.yml up -d

From then on the routing config is committed YAML with no "do not edit" banner and nothing to regenerate. The only thing it still relies on is the shared edge from unport init, which lives at the machine level.

Good to know

  • macOS and Linux are supported. On macOS the resolver goes in /etc/resolver; on Linux you point systemd-resolved or dnsmasq at 127.0.0.1. Windows is not supported yet.
  • Dev servers like Vite and Angular block hostnames they do not recognize on purpose. unport spots those and routes them so the server sees a trusted loopback address instead, and frontend.myapp.test loads with no change to your vite config. Opt a service out with x-unport.devServer: false.
  • Corporate VPNs are the most common reason .test resolution breaks, because some of them capture every DNS query. unport doctor checks for this and names the VPN when it finds one.
  • unport doctor also notices when another local proxy (portless, Caddy, a forgotten dev server) is sitting on 80 or 443 and would answer in place of the edge. That failure is otherwise a confusing wrong-page-404, so it is worth catching by name.
  • The default suffix is .test, which is reserved for local use and never resolves on the public internet. Change it with --tld if you need to.

Requirements

  • Docker, with Compose v2 (docker compose, not the old docker-compose).
  • Node.js 24 or newer.
  • macOS or Linux.

Development

git clone https://github.com/jotarios/unport.git
cd unport
npm install
npm test
# compile to dist/
npm run build

The decisions (planning, the override, port allocation, conflict detection) live in src/core and are tested directly, apart from any IO. src/commands wires them to the CLI, and DESIGN.md records the reasoning behind the parts that were not obvious.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages