If you have ever launched two Docker containers and tried to make one talk to the other, you have probably hit the same wall I did the first time. You connected to the first container, typed ping db, and watched the request die with ping: bad address 'db'. The container was right there, two seconds old, and yet its name meant nothing. That moment is the moment this guide is built around.
Configuring a Docker network so containers can talk to each other by name is one of those tasks that is simple once you know the trick and baffling before. The trick is that Docker’s default bridge network does not give you the DNS behavior most people expect. The good news is that the fix is a single command, and once you understand it, you can build multi-container apps, microservices, and full Docker Compose projects without ever hardcoding an IP address.
In this guide I will walk you through everything I have learned from running dozens of local stacks and a few production clusters. You will get the exact commands, real diagnostic output, and the trade-offs I have hit personally between user-defined bridges, network aliases, and Compose.
Table of Contents
Why Default Bridge Networking Breaks Container Name Resolution?
The first thing to understand is why the default behavior feels broken. When you run docker run --name db postgres and then docker run --name api my-api, both containers land on the same default bridge network. They can technically reach each other by IP, but they cannot reach each other by name. The reason is historical: the default bridge is a legacy network designed before Docker had a built-in DNS resolver for containers.
What that means in practice is that inside the api container, the embedded DNS server only knows about the host’s DNS and the bridge’s own gateway. It does not know that db exists. The most common symptom is something like could not translate host name "db" to address: Name or service not known when your application tries to connect to a database.
Here is the comparison I wish someone had shown me on day one.
Default Bridge vs User-Defined Bridge
Automatic DNS for container names: Default bridge = No. User-defined bridge = Yes. This is the single most important difference. On a user-defined bridge, the embedded DNS server automatically registers every container by name, so ping db just works.
Container-to-container communication: Default bridge = By IP only. User-defined bridge = By name or IP. On a user-defined bridge, your application can use the container name in connection strings and never worry about IP changes.
Network isolation between groups: Default bridge = No. User-defined bridge = Yes. Every user-defined network is isolated from every other user-defined network by default, which is exactly what you want for security.
Support for network aliases: Default bridge = No. User-defined bridge = Yes. Aliases let a single container be reachable under multiple names, which is useful during migrations and multi-role services.
Works on the same host: Both = Yes. There is no operational difference here; both network types work on a single Docker host.
Recommended for new projects: Default bridge = No. User-defined bridge = Yes. Docker’s official documentation is unambiguous on this point.
Docker’s official docs make this explicit: user-defined bridges are the recommended path for any multi-container workload. The default bridge still exists for backward compatibility, and that is the only reason it is still around.
How to Create a User-Defined Bridge Network?
The fix is one command. Creating a user-defined bridge network gives you a bridge that runs Docker’s embedded DNS server, and that server automatically registers every container you attach to it.
To create a network called app-net, run:
docker network create app-netThat is the default behavior. Docker picks an unused subnet from the 172.x range, creates a bridge, and binds the embedded DNS server to it. You can verify it exists with:
docker network ls
docker network inspect app-netIf you want to control the subnet, gateway, or IP range, you can pass them explicitly. The following example creates a network on a predictable subnet, which is useful when you are pinning container IPs or integrating with other tools.
docker network create
--driver bridge
--subnet 172.20.0.0/16
--gateway 172.20.0.1
app-netI use named networks with the suffix -net everywhere. It is a small habit but it pays off when you start running five or six networks in parallel: frontend-net, backend-net, db-net, and so on. You can always see at a glance what a container is wired to.
How to Launch Containers on Your Custom Network?
With the network in place, the next step is to attach containers to it. The --network flag on docker run connects a container to your user-defined network at start time. From that moment, the embedded DNS server starts indexing the container.
Here is a minimal example with two containers that need to talk to each other by name:
docker run -d --name db --network app-net postgres:16
docker run -d --name api --network app-net my-api:latestInside the api container, the hostname db now resolves to the database’s IP. Your application can connect with a connection string like postgres://db:5432/myapp and it will work. If you restart the database, the IP may change, but the name keeps resolving because the embedded DNS updates as containers come and go.
You can confirm both containers are on the same network with:
docker network inspect app-netThe output includes a Containers block listing every container by ID, name, and IP. If a container is missing from that list, it is not on the network, and DNS resolution will not work for it.
If you already started a container on the default bridge and want to attach it without recreating it, docker network connect does the job:
docker network connect app-net existing-containerThis is the modern replacement for the old --link flag, which is deprecated and only worked on the default bridge. I will repeat that: --link is deprecated. If you see old Stack Overflow answers using it, ignore them and use a user-defined network instead.
Testing Container-to-Container Communication by Name
Testing is the part most guides skip, and it is the part where you actually learn what is going on. There are three commands I reach for every time, and they cover 95 percent of the cases I run into.
The first is getent hosts, which queries the system resolver and prints the IP. It is more reliable than ping because minimal images often do not ship ping.
docker exec api getent hosts dbExpected output is a single line like 172.20.0.2 db. If you get nothing, the DNS resolution is broken. If you get an IP, the network is working.
The second is curl for HTTP services. If you have a web server in the api container, hit it from another container:
docker exec web curl -s http://api:8080/healthThe third is nc (netcat) for raw TCP. It is handy when you suspect a port is open but a service is not responding:
docker exec api nc -zv db 5432Expected output is something like db (172.20.0.2:5432) open. If the port is closed, you will see Connection refused, which usually means the process in the target container is not listening on that port, not that the network is broken.
I run these three commands in this order whenever something is not connecting. They give me a fast yes/no on whether the issue is DNS, routing, or the application itself.
Configuring a Docker Network with Docker Compose
Docker Compose is where all of this stops feeling manual. When you define a docker-compose.yml with multiple services, Compose automatically creates a default network for the project. Every service joins that network, and every service name becomes a hostname automatically. You do not need to run docker network create or pass --network flags.
Here is a small example that puts it all together:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
volumes:
- db-data:/var/lib/postgresql/data
api:
image: my-api:latest
environment:
DATABASE_URL: postgres://db:5432/myapp
depends_on:
- db
volumes:
db-data:Inside the api container, db resolves to the database container’s IP. There is no extra configuration. If you scale api to three replicas, all three talk to db the same way.
For more complex apps, you usually want named networks. A common pattern is a frontend network and a backend network, with the API sitting on both: the frontend talks to it via the frontend network, and the database talks to it via the backend network. The database never joins the frontend network directly.
services:
web:
image: my-web:latest
networks:
- frontend
api:
image: my-api:latest
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
backend:This is the layout I default to for any non-trivial project. It is the kind of design that scales from a laptop to a production cluster without changing a thing in the application code.
Network Aliases, Custom Hostnames, and Multiple Identities
Sometimes a container needs to be reachable under more than one name. Maybe you are migrating a service from legacy-db to db and want both names to work during the transition. Maybe you have a Redis container that serves as both a cache and a queue, and you want apps to refer to it as cache or queue depending on the role. That is what network aliases are for.
With the Docker CLI, you add an alias with the --network-alias flag:
docker run -d --name redis --network app-net --network-alias cache --network-alias queue redis:7Now any container on app-net can reach this Redis as redis, cache, or queue. The first name is the container name, and the aliases are added on top.
In Docker Compose, you set aliases under the networks key for a service:
services:
redis:
image: redis:7
networks:
app-net:
aliases:
- cache
- queueAliases are additive across networks. If a container is attached to three networks, each network can have its own set of aliases. This is how you run a single container that presents as different services to different consumers, which is a common pattern in microservices.
Published Ports vs Container-to-Container Ports
The most common confusion I see in forum threads is the difference between -p and what containers use to talk to each other. They are not the same thing, and conflating them causes hours of debugging.
The -p 8080:80 flag (or the ports: key in Compose) tells Docker to publish port 80 of the container on port 8080 of the host. That is for traffic from your laptop or the outside world. Containers on the same network do not need any port publishing at all. They communicate on the container’s internal port directly.
So if your web container listens on port 3000 internally, your API container connects to it as http://web:3000, with no published port involved. The -p flag is only relevant when traffic crosses the host boundary.
The practical rule: databases should never publish host ports for internal traffic. If you bind Postgres to 0.0.0.0:5432 on your laptop, you have just exposed your dev database to anyone on the same network. For internal services, leave the ports field out entirely and let containers talk to each other over the network.
depends_on vs Health Checks: The Readiness Gap
One of the most common pitfalls, especially with Docker Compose, is assuming depends_on waits for a service to be ready. It does not. It only waits for the container to be started. A Postgres container can be “started” in 200 milliseconds while Postgres itself is still initializing. Your API then tries to connect and fails.
The fix is to combine depends_on with a health check. Compose supports the condition: service_healthy option, which blocks the dependent container until the dependency reports healthy.
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
image: my-api:latest
depends_on:
db:
condition: service_healthyNow the api container waits until Postgres is actually accepting connections. This tiny change removes a class of flaky startup bugs that I have seen bring down entire CI pipelines. If you take one thing away from this section, let it be this: depends_on is for ordering, not readiness.
Troubleshooting Common Docker Network Problems
When containers cannot talk to each other, the list of possible causes is short but the symptoms all look the same. Here is the diagnostic workflow I use, in order.
Confirm both containers are on the same network. Run
docker network inspect app-netand check theContainersblock. If a container is missing, it is not on the network.Confirm DNS resolution works. Run
docker exec api getent hosts db. If it returns an IP, DNS is fine. If it returns nothing, the resolver cannot find the name, which usually means the container is on the wrong network.Check the port from inside the network. Run
docker exec api nc -zv db 5432. If it says “open”, the service is reachable. If it says “Connection refused”, the process is not listening on that port yet.Confirm the application is using the hostname, not localhost. A connection string of
localhost:5432inside a container points to the container itself, not to a sibling. Use the service name.Check firewall and SELinux or AppArmor rules. On Linux hosts, firewall rules can block inter-container traffic. The Docker-related chains usually allow it, but custom iptables rules can break things.
Look for DNS caching issues. Some containers cache DNS results. Restarting the container usually clears the cache. The embedded DNS itself updates in real time.
Most issues fit one of these buckets. The top three I see in practice are: the container is on the default bridge instead of a user-defined network, the application is using localhost instead of the service name, and the database is not ready when the consumer starts. The diagnostic commands above catch all three in under a minute.
Best Practices for Docker Network Design
After running dozens of stacks, a few patterns have earned a permanent spot in my defaults.
First, create one user-defined network per logical grouping. Frontend, backend, and data services get their own networks. The data network never touches the frontend network directly. This is simple, resilient, and trivially debuggable.
Second, use service names in code and configuration, never IPs. Connection strings like postgres://db:5432/myapp are portable across environments. The same Compose file runs on your laptop, your CI runner, and your staging cluster without changes.
Third, keep databases off the host network. Bind addresses like 0.0.0.0 for a database in development are a security smell. Use the default 127.0.0.1 inside the container and let other containers reach it via the network. There is no need to expose database ports to the host.
Fourth, lean on Docker Compose for anything with more than two containers. The manual docker network create workflow is fine for learning, but in real projects you want a single source of truth that recreates the network from a YAML file. Compose’s named networks give you that, plus aliases, plus health checks, in one place.
Finally, treat the network as part of your deployment surface. Version control your Compose files. Document the network names. Print the cheat sheet below and pin it next to your terminal. Small habits compound, and the network layer is where most downtime in containerized systems starts.
Frequently Asked Questions
How do I make Docker containers communicate with each other?
Create a user-defined bridge network with docker network create app-net, then launch each container with u002du002dnetwork app-net. The embedded DNS server will register every container by name so they can resolve each other without hardcoded IPs.
Can Docker containers talk to each other by name?
Yes, but only on user-defined bridge networks or networks created by Docker Compose. The default bridge network does not provide DNS-based name resolution, so containers on it can only reach each other by IP address.
How do I connect two Docker containers?
Run docker network create my-net, then start each container with u002du002dnetwork my-net and a u002du002dname. Use docker network inspect my-net to confirm both containers are attached. From inside one container, the other’s name resolves automatically.
What is the difference between default bridge and user-defined bridge in Docker?
The default bridge is a legacy network that does not provide automatic DNS for container names. User-defined bridges run Docker’s embedded DNS server, which automatically resolves container names and aliases across containers on the same network. User-defined bridges also enable better isolation and support for network aliases.
How do I use Docker Compose to enable container communication?
Define your services in a docker-compose.yml file. Compose automatically creates a default network for the project, and every service name becomes a hostname. For complex layouts, define named networks under the top-level networks key and attach services to them explicitly.
Why can’t my Docker containers resolve each other’s hostnames?
The most common cause is that both containers are on the default bridge. Default bridge networks do not provide DNS-based name resolution. Create a user-defined bridge with docker network create and attach both containers to it, and the embedded DNS will resolve names automatically.
How do I configure DNS for Docker containers?
On user-defined bridge networks, Docker’s embedded DNS server handles name resolution automatically. To customize, you can pass u002du002ddns to docker run, set DNS options in the daemon.json file, or use Compose’s dns and dns_search keys for individual services.
Quick Reference Cheat Sheet
Keep these commands handy. They cover roughly 90 percent of the day-to-day work with Docker networks.
# Create a user-defined bridge network
docker network create app-net
# Create a network with explicit subnet and gateway
docker network create --driver bridge --subnet 172.20.0.0/16 --gateway 172.20.0.1 app-net
# Run a container on a network
docker run -d --name db --network app-net postgres:16
# Attach an existing container to a network
docker network connect app-net existing-container
# Detach a container from a network
docker network disconnect app-net existing-container
# Inspect a network (shows containers, subnets, gateway)
docker network inspect app-net
# List all networks
docker network ls
# Remove a network (containers must be detached first)
docker network rm app-net
# Add a network alias when running a container
docker run -d --name redis --network app-net --network-alias cache redis:7Inside any container on the same network, you can verify name resolution with getent hosts <name> and test connectivity with curl http://<name>:<port> or nc -zv <name> <port>. Those three commands have saved me more time than any other debugging pattern in my workflow.
Conclusion
Configuring a Docker network so containers can talk to each other by name is the foundation of every reliable multi-container setup. The default bridge network is a legacy relic that does not give you the DNS behavior you expect, and the moment you switch to a user-defined bridge, the embedded DNS server takes over and the names just work. From there, Docker Compose removes most of the manual wiring, and network aliases give you the flexibility to handle migrations and multi-role services without changing application code.
Start with a single user-defined network for your project, name your containers clearly, and use those names in your connection strings. If something breaks, walk through the diagnostic workflow: same network, DNS resolution, port reachability, hostname in code. The answer is almost always one of those, and the fix is usually a single command.
Bookmark the cheat sheet above, print it if you have to, and the next time a teammate asks “why can’t my containers find each other”, you will have the answer before they finish the sentence.