Why Docker Networks Matter When You Have More Than One Container
As soon as your project has more than one service (for example, a web app plus a database, or an API plus a cache), the containers need a reliable way to find and talk to each other. Docker networks provide that communication layer. Instead of hard-coding IP addresses or exposing every internal port to your host machine, you connect containers to the same Docker network and let Docker handle service discovery and routing.
A Docker network is a virtual network managed by Docker. Containers attached to the same network can communicate using IP addresses, but more importantly, they can communicate using container names (DNS-based service discovery). This is the key beginner-friendly idea: on a user-defined Docker network, Docker runs an internal DNS server so that http://api:3000 can resolve to the container named api without you needing to know its IP.
Network Drivers You Will Use as a Beginner
Docker supports multiple network drivers. For beginner projects, you will mostly use:
- bridge: The default for single-host container networking. A user-defined bridge network gives you automatic DNS by container name and better isolation than the default
bridgenetwork. - host: The container shares the host’s network stack. This removes network isolation and is not ideal for most beginner multi-service setups; it can be useful for troubleshooting or special performance needs.
- none: No networking. Useful for highly restricted containers, not for connecting services.
In this chapter, the focus is on user-defined bridge networks, because they are the simplest way to connect services on one machine while keeping a clean separation between “internal service-to-service traffic” and “ports exposed to your host.”
Key Concepts: Internal Ports vs Published Ports
When connecting services, it helps to separate two ideas:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
- Internal container ports: The ports a service listens on inside the container (for example, PostgreSQL listens on 5432, Redis on 6379, many Node apps on 3000). Containers on the same Docker network can reach these internal ports directly.
- Published ports: Ports you map from your host to a container using
-p HOST:CONTAINER. Publishing is only needed when you want to access a container from outside Docker (for example, your browser on the host). It is not required for container-to-container communication on the same network.
A common beginner mistake is publishing every service port “just in case.” Instead, publish only what you need to access from the host (often just the web entrypoint). Keep databases and caches internal to the network unless you have a specific reason to expose them.
Creating and Inspecting a User-Defined Network
Start by creating a dedicated network for your project. This gives you predictable DNS behavior and isolates your services from other containers.
docker network create app-netList networks to confirm it exists:
docker network lsInspect the network to see its configuration and attached containers:
docker network inspect app-netAt first, the Containers section will be empty. As you attach containers, you will see their IP addresses and endpoints inside this network.
Step-by-Step: Connect a Web App to a Database Using a Docker Network
This example demonstrates the core workflow: create a network, run a database container on it, run an app container on it, and configure the app to connect to the database using the database container name as the hostname.
Step 1: Create the network
docker network create app-netStep 2: Run a database container attached to the network
Here is a PostgreSQL example. Notice that we do not publish port 5432 to the host. The database is intended to be reachable only by other containers on app-net.
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=secret postgres:16Docker will attach db to app-net and register the name db in the network DNS.
Step 3: Run an application container on the same network
Assume your app expects a connection string in an environment variable (many frameworks do). The important part is the hostname: use db, not localhost. Inside a container, localhost refers to that same container, not the database container.
docker run -d --name web --network app-net -p 8080:8080 -e DATABASE_HOST=db -e DATABASE_PORT=5432 my-web-image:latestNow the web container can connect to db:5432 over the internal Docker network. Your browser can reach the web app at http://localhost:8080 because only the web service is published to the host.
Step 4: Verify name resolution and connectivity from inside the container
To confirm that DNS works, you can run a quick command inside the web container. Not all images include diagnostic tools, but you can still verify resolution using basic utilities if present. If your image is minimal, you can use a temporary troubleshooting container on the same network.
Option A: Use a temporary container with networking tools:
docker run --rm -it --network app-net alpine:3.20 shInside the shell, try resolving the database name:
getent hosts dbIf getent is not available, you can install tools temporarily (for troubleshooting only):
apk add --no-cache bind-toolsnslookup dbThen test TCP connectivity (again, you may need to install a tool):
apk add --no-cache busybox-extrasnc -zv db 5432The key learning: if the containers share the same user-defined network, the name db resolves and traffic routes without exposing the database port to the host.
Understanding Docker’s Built-In DNS on User-Defined Networks
On a user-defined bridge network, Docker provides an embedded DNS server. When a container asks to resolve a name, Docker can answer with the IP of another container on the same network. This is why container names become stable hostnames within that network.
Important details:
- DNS-based discovery works reliably on user-defined networks. The default
bridgenetwork behaves differently and is less convenient for name-based discovery. - The hostname is typically the container name (for example,
db). You can also add extra names using network aliases. - Container IP addresses can change when containers are recreated. Rely on names, not IPs.
Network Aliases: One Service, Multiple Names
Sometimes you want a stable service name even if the container name changes, or you want multiple names pointing to the same container (for example, postgres and db). You can add aliases when connecting a container to a network.
Example: run a database container and give it an alias postgres on app-net:
docker run -d --name db --network app-net --network-alias postgres -e POSTGRES_PASSWORD=secret postgres:16Now other containers can connect to db or postgres as the hostname.
Connecting an Existing Container to Another Network
You are not limited to attaching a container to a network at startup. You can connect and disconnect networks dynamically.
Connect an existing container to a network:
docker network connect app-net some-containerDisconnect it:
docker network disconnect app-net some-containerThis is useful when you realize a container needs access to a service network, or when you want to temporarily attach a debugging container to inspect traffic.
Multi-Network Patterns: Frontend and Backend Separation
A practical pattern is to use two networks:
- A frontend network for traffic between a reverse proxy (or web entrypoint) and your app services.
- A backend network for traffic between app services and internal dependencies like databases and caches.
This gives you a simple form of segmentation. For example, your database is only on the backend network, so the reverse proxy cannot talk to it directly.
Create two networks:
docker network create front-netdocker network create back-netRun a database only on the backend:
docker run -d --name db --network back-net -e POSTGRES_PASSWORD=secret postgres:16Run an API on both networks (it needs to accept requests from the frontend side and talk to the database on the backend side):
docker run -d --name api --network front-net -e DATABASE_HOST=db -e DATABASE_PORT=5432 my-api-image:latestThen connect the API to the backend network as well:
docker network connect back-net apiNow api can receive traffic from containers on front-net and can reach db on back-net. The database remains isolated from the frontend network.
Step-by-Step: Simple API + Redis Cache on a Shared Network
Another common beginner setup is an API that uses Redis. The networking idea is the same: attach both containers to the same user-defined network and use the Redis container name as the hostname.
Step 1: Create a network
docker network create cache-netStep 2: Run Redis (internal only)
docker run -d --name redis --network cache-net redis:7Step 3: Run your API and point it at Redis
docker run -d --name api --network cache-net -p 3000:3000 -e REDIS_HOST=redis -e REDIS_PORT=6379 my-api-image:latestFrom the API container’s perspective, redis:6379 is reachable because both containers are on cache-net. You only publish the API port to the host so you can test it from your browser or a REST client.
Troubleshooting Container-to-Container Networking
When services cannot connect, the issue is usually one of these: wrong hostname, wrong port, wrong network, or the service is not listening yet. Use a structured approach.
1) Confirm both containers are on the same network
Inspect the network and check the container list:
docker network inspect app-netOr inspect a container and look at its networks:
docker inspect webLook for the Networks section and confirm the expected network is present.
2) Confirm you are not using localhost incorrectly
If your app uses localhost to reach the database, it will try to connect to itself. In multi-container setups, the hostname should be the other container’s name (for example, db) or a network alias.
3) Confirm the target service is listening on the expected port
Inside the target container, check logs or run a command if available. Often the simplest check is to view logs:
docker logs dbIf the database is still starting up, your app may fail on the first attempt. Many apps need retry logic or a startup wait strategy.
4) Use a temporary debug container on the same network
This is a reliable technique because it avoids modifying your production-like images. Start a shell on the same network and test DNS and connectivity:
docker run --rm -it --network app-net alpine:3.20 shThen resolve and test ports as shown earlier (nslookup, nc).
5) Check whether you accidentally relied on published ports
Published ports are for host-to-container access. Container-to-container access should use the container’s internal port. For example, if you publish -p 15432:5432 for PostgreSQL, other containers should still connect to db:5432, not db:15432.
How Docker Compose Uses Networks (Practical Mental Model)
Even if you are not using Docker Compose yet, it helps to understand the mental model because many multi-service projects use it. Compose automatically creates a user-defined bridge network for your application and connects all services to it (unless you configure otherwise). That is why, in Compose setups, services can typically reach each other by service name.
The key takeaway you can apply even without Compose is: create a user-defined network, attach related containers to it, and use names for connectivity.
Security and Exposure: Keep Internal Services Internal
Networking is not only about connectivity; it is also about reducing unnecessary exposure.
- Do not publish database ports unless you need host access for a tool. Prefer keeping them internal and using a one-off container for admin tasks on the same network.
- Use separate networks to prevent unrelated containers from reaching sensitive services.
- Prefer explicit networks per project instead of attaching everything to a shared network.
A practical workflow for admin access without publishing a database port is to run a temporary client container on the same network. For PostgreSQL, for example, you could run a client container attached to app-net and connect to db by name. This keeps the database unexposed to the host network while still allowing controlled access from within Docker.
Common Beginner Pitfalls (and How to Avoid Them)
Using container IP addresses
Container IPs can change. Use container names or network aliases. If you need a stable name like database, set an alias and keep your app configuration pointing to that alias.
Mixing default bridge and user-defined networks
If one container is on the default bridge network and another is on app-net, they cannot talk by name and may not be able to talk at all. Put related services on the same user-defined network.
Publishing ports for internal dependencies
Publishing is not required for container-to-container communication. Publish only the entrypoints you need from the host (often one HTTP port).
Forgetting that startup order is not readiness
Even if you start the database container first, it may take time to initialize. Your app should handle retries, or you should implement a wait strategy. Networking can be correct while the service is still unavailable.