Problem
Hardening an inference stack, I put the model server on an internal: true bridge (no egress) and published the API as 127.0.0.1:11435:11434 so the host could call it while the world could not. docker compose up accepted the file. docker ps showed 0.0.0.0:…/127.0.0.1:11435->11434/tcp. Then curl http://127.0.0.1:11435/api/version got connection refused — forever.
This is the trap: on an internal network there is no published port at all, and Docker does not say so. The userland proxy never binds, no NAT rule is created, and nothing in the standard tooling flags it. Anyone who builds a "loopback-isolated" API on an internal bridge is running an API that does not exist — a silent availability-and-security misconfiguration.
Investigation
docker inspect told the story plainly once I stopped trusting docker ps:
HostConfig.PortBindings: {"11434/tcp":[{"HostIp":"127.0.0.1","HostPort":"11435"}]}
NetworkSettings.Ports: {"11434/tcp":null} ← declared, nothing bound
No ss listener on the host port, and the process inside the container is listening correctly on :11434 (its internal healthcheck returns 200 from inside). The gap is purely the Docker-side publish step — deliberately skipped for internal networks, silently.
Fix
Stop trying to publish ports on an internal network. The host is an L2 peer on the bridge, so it can reach the container at a deterministic address directly:
- Give the bridge a fixed static subnet (IPAM) and the container a fixed IP.
- Publish nothing. The API endpoint is the container IP itself.
- Audit against
HostConfig.PortBindings == {}rather than trustingdocker ps.
There is no docker-proxy process, nothing bound to loopback, and the 172.30.1.0/24 subnet is never routed or advertised — the model endpoint is host-local, and more locked down than 127.0.0.1 publishing would have been.
Verification
curl http://172.30.1.2:11434/api/versionreturns 200 from the host.- Outbound TCP from the container still fails (egress blocked). ✓
- The audit's live port check passes:
PortBindingsis{}.
Lesson
docker ps port columns are a claim, not a fact. On internal: true networks the claim is silently not honored. Verify with docker inspect -f '{{json .HostConfig.PortBindings}}' and a real connection attempt — never with the docker ps display.