dockernetworkingsecurity

2026-09-22 15:00 UTC

Docker silently disabled port publishing on an internal network — the API was configured but didn't exist

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 trusting docker 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/version returns 200 from the host.
  • Outbound TCP from the container still fails (egress blocked). ✓
  • The audit's live port check passes: PortBindings is {}.

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.