docker run starts cleanly and the logs look healthy — "Uvicorn running",
"Application startup complete" — but every curl http://127.0.0.1:8000/health
from the laptop fails with connection refused or resets instantly. The service
answered perfectly five minutes ago when it ran outside Docker with the same
command. Nothing crashes, nothing errors in the logs; the container simply serves
no one.
The first run of a containerised FastAPI service, fresh from a lesson or tutorial
where uvicorn app.main:app worked directly on the laptop. Two habits from bare-
laptop serving cause it: uvicorn's default bind address is 127.0.0.1, which was
exactly right outside Docker, and on the laptop there was no reason to think
about publishing ports, so -p is easy to omit. Both mistakes produce the same
"it runs but nothing connects" feel, which is why they burn hours. Linked
modules: 4.3 (Docker for ML), 4.2 (Serving with FastAPI).
A container has its own network, so two doors must both be open: the server must
listen on an address reachable from outside the container, and Docker must map a
host port to the container port. Cause one — bound to 127.0.0.1 inside the box:
loopback inside a container means "this container talking to itself"; the log
line "running on http://127.0.0.1:8000" is the tell. Cause two — port never
published: docker run without -p leaves the PORTS column of docker ps
empty, so no laptop port leads inside. Split them with one experiment:
docker exec <ctr> curl -s http://localhost:8000/health (or python urllib if
curl is absent). If that answers, the server is fine and the mapping is missing —
check docker ps. If even the inside call fails on 0.0.0.0-bound apps, the
server itself is not up on that port. Read the bind address in the logs, read
PORTS in docker ps, test from inside: three reads, full diagnosis.
Bind to all interfaces inside the container and publish the port at run time.
In the Dockerfile:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] —
baking the host into CMD means no one can forget it at 2 a.m. On the run side:
docker run -p 8000:8000 churn-service (host port left of the colon, container
port right — -p 9000:8000 serves the same container on laptop port 9000).
Verify the repair end to end: logs now read "running on http://0.0.0.0:8000",
docker ps shows 0.0.0.0:8000->8000/tcp, and the laptop curl of /health
returns {"status":"ok","model_loaded":true}. Note that 0.0.0.0 belongs inside
the container; the exposure to the outside world stays controlled by -p.
Adopt the three-step connectivity checklist for any unreachable container: bind
address in the logs, PORTS column in docker ps, then a curl from inside via
docker exec. Keep --host 0.0.0.0 in CMD so images are correct by
construction, and keep a documented run command (README or compose file) so -p
travels with the project instead of living in someone's shell history. When a
teammate reports "the container is broken", ask for the docker ps line before
anything else — it answers half of these reports on its own.
Q: "Your containerised API runs, the logs are clean, but requests from the host
fail. Diagnose it." Strong answer: name both doors — bind address inside the
container and port publishing outside — and the tell for each (127.0.0.1 in the
logs; empty PORTS in docker ps). Give the splitting experiment: curl from
inside the container via docker exec, which cleanly separates "server not
listening correctly" from "port not mapped". Bonus signal: explaining why
loopback is the right default outside Docker and the wrong one inside, and that
0.0.0.0 inside a container is safe because -p remains the actual gate.