Once more than one container exists, networking becomes one of the most practical Docker skills.
Teams need to know how services discover each other, which ports are internal versus exposed, and where traffic enters the system.
Beginners often mix host ports and container ports mentally, which leads to confusion. Professionals care about network boundaries, naming, and service clarity.
A good container network design keeps communication predictable without exposing more than necessary.
A very useful distinction is separating container-to-container traffic from traffic that needs to reach the host machine or outside users. Many services only need to talk internally and should not be publicly exposed.
When beginners publish every port out of convenience, they lose sight of this difference. Internal communication and external access deserve different decisions.
In containerized systems, service discovery should rely on stable names or service definitions rather than temporary runtime IP addresses. This keeps multi-service setups more resilient and easier to understand.
Professionals want traffic paths that survive restarts and recreations without manual reconfiguration. Service names are usually the friendlier stable contract.
A container has its own network namespace. Inside a container, localhost refers to that same container, not the host and not another service. Put related containers on a user-defined bridge network and connect by container or service DNS name. Docker supplies DNS resolution for names on that network.
Container ports describe where an application listens. Publishing a port maps a host address and port to the container port so callers outside the Docker network can connect. Container-to-container traffic normally uses the private service name and container port without publishing the database or cache to the host.
An application must listen on the container interface, commonly 0.0.0.0, rather than only 127.0.0.1. Test DNS, connection, and application protocol separately. A successful name lookup does not prove the process is listening, and an open TCP port does not prove the application is ready.
Networking bugs often feel mysterious because the app itself may be healthy while traffic still fails to reach it. Good debugging begins by checking one hop at a time: is the process listening, is the port correct, is the service on the right network, and is the caller using the right destination?
This hop-by-hop method is more reliable than changing random port mappings and hoping something starts working.
Place an API and database on one user-defined network and connect by service name. Then access the API from the host through a published port to distinguish container-to-container traffic from host-to-container traffic.
Work through this as a controlled engineering exercise rather than a copy-and-paste demo. State the expected result before running anything, keep the input small enough to inspect, and record the important intermediate state. That makes the lesson explain not only what to type, but why the result is trustworthy.
Using localhost inside the API points back to the API container, not the database. A published port is normally unnecessary for private service-to-service communication.
Verification must use evidence that matches the concept. Resolve the database service name inside the API container, inspect network membership, and test the host-facing port separately. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.
Use separate networks to limit communication paths, such as edge-to-web and web-to-data. Attaching every container to one broad network weakens isolation. Docker networks are not a complete zero-trust policy, so production platforms may need stronger network policy, identity, and service authorization.
Preserve TLS and identity according to trust boundaries. Internal traffic is not automatically trustworthy. Validate certificates and hostnames for sensitive dependencies, rotate credentials, and avoid embedding secrets in connection URLs printed to logs. Set connection, read, and request deadlines so a failed dependency does not hold resources indefinitely.
Inspect network membership, DNS resolution, routes, listening sockets, and logs. Reproduce from inside the caller container because host connectivity can differ. Monitor connection errors, pool saturation, retransmissions, and dependency latency. Avoid relying on fixed container IPs because replacement changes them.
This is the sort of route a developer should be able to describe clearly.
Browser -> published web service port -> app container -> internal database service name on shared Docker network
Adapt this focused example to a disposable local environment and inspect every result before expanding it.
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run --rm --network appnet busybox nslookup db
docker network inspect appnet
The database is discoverable to the API without a host port.
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name api --network appnet -p 8080:8080 \
-e DATABASE_HOST=db example/api:1.0
docker run --rm --network appnet busybox nslookup db
docker network inspect appnet
Check resolution, socket reachability, and HTTP behavior in order.
docker exec api getent hosts db
docker exec api sh -c \"nc -vz db 5432\"
docker exec api sh -c \"wget -S -O- http://web:8080/health\"
docker logs --tail=100 api
docker inspect api --format \"{{json .NetworkSettings.Networks}}\"
No. If they share the right Docker network, they can often communicate internally without exposing those ports to the host.
Inside container networks, the service name usually represents the correct destination. `localhost` inside one container refers only to that same container.
Explore 500+ free tutorials across 20+ languages and frameworks.