Skip to main content
CodeOath
← All posts

Docker105 min total · 19 parts

Docker Fundamentals: Images, Containers, and Writing a Good Dockerfile

Part 11 of 19 · ~3 min

Networking Basics

Getting worker talking to Redis and Postgres is the next wall you hit, and it surfaces a default nobody explains until it bites you.

Nothing reaches a container from outside it until you explicitly publish a port. EXPOSE 8080 in a Dockerfile is a note to future readers, not an action — by itself it opens exactly nothing. The thing that actually opens a port happens later, at docker run:

docker run -p 8080:3000 snapledger-web
# host:container — traffic to localhost:8080 is forwarded to port 3000 inside the container

There is exactly one place EXPOSE's metadata does something on its own: docker run -P (capital P, no port list) publishes every port the image's EXPOSE lines declared, each to a random free port on the host. It's a narrow case — mostly useful for quickly spinning up an ad hoc container without deciding on a port mapping — but it's not nothing, and it's worth knowing EXPOSE isn't purely inert.

Publishing correctly is only half of it, and Marcus finds the other half the first time web runs on staging instead of a laptop. -p 3000:3000 is set, the container is Up, and hitting it from outside returns nothing but connection refused — while docker execing in and curling localhost:3000 from inside works perfectly. The Express app was written with app.listen(3000, "127.0.0.1", ...), binding only to the container's own loopback interface. Traffic arriving through the published port comes in on the container's external network interface, never touches loopback, and there's nothing listening there to answer it. Binding to 0.0.0.0 instead — all interfaces, not just loopback — is what makes a published port actually reachable, and it's a one-word change that's easy to get backwards, since 127.0.0.1 is the instinctive, "safe-looking" choice for anyone used to running a server directly on a laptop rather than inside a container.

Put containers on a network you created yourself, and they can find each other by name alone — no publishing, no port mapping, nothing extra required. Docker runs its own small DNS resolver on every user-defined network, and it's what turns a container name into a working address for anything else sitting on that same network:

docker network create snapledger-net
docker run --network snapledger-net --name redis -d redis:7
docker run --network snapledger-net --name worker snapledger-worker
# from inside "worker", the hostname "redis" resolves straight to the redis container

This is exactly what trips you up the first time. Your first attempt at wiring worker to Redis is two plain docker run commands, no --network flag on either, and worker fails on its very first connection attempt:

Error: getaddrinfo ENOTFOUND redis
    at GetAddrInfoReqWrap.onlookup [as oncomplete]

Nothing about that error mentions networks, containers, or Docker at all — it reads exactly like a typo, and your first instinct is to double-check you spelled redis correctly everywhere. You did. Neither container was ever told to join snapledger-net, or any network you'd made yourself, so Docker quietly defaulted both of them onto the plain bridge network it ships with — and that particular network has no resolver running on it at all, unlike one you create explicitly. redis isn't a typo; there's simply nothing on that network that answers to the name. Add --network snapledger-net to both docker run commands, keep the connection string exactly as it was, and it resolves, because now there's an actual name server watching that network for it. Compose, once you reach for it two chapters from now, sets up its own network the same way automatically, which is why nobody using it ever has to type docker network create for it to feel like service names "just work."