

Sarthak Varshney is a Docker Captain, 5x C# Corner MVP, and 2x Alibaba Cloud MVP, with over six years of hands-on experience in the IT industry, specializing in cloud computing, DevOps, and modern application infrastructure. He is an Author and Associate Consultant, known for working extensively with cloud platforms and container-based technologies in real-world environments.
You have three containers running — one is your Node.js app, one is your MySQL database, and one is your Redis cache. Now here's the question that trips up almost every Docker beginner: how do these containers actually talk to each other? And more importantly, how do you make sure they're talking to the right things — and not exposing stuff to the outside world that shouldn't be out there?
That, my friend, is Docker networking. And once it clicks, you'll never look at docker run the same way again.
Think of Docker networking like the wiring inside an office building. Some rooms are connected to each other privately (nobody from outside can walk in). Some have a direct door to the street. And some are in total isolation — no phone, no network, nothing.
Docker gives you different "wiring options" depending on what you need. These options are called network drivers, and the three you absolutely need to know are:
bridge — the default, shared private networkhost — uses your actual machine's network directlynone — complete isolation, no network at allPlus there are custom networks that give you superpowers like DNS-based container discovery. Let's go through them one by one.
When you run a container without specifying any network, Docker automatically drops it into the default bridge network. It's like a shared apartment building — all containers are in the same building, but they're in separate rooms.
docker run -d --name my-nginx nginx
docker inspect my-nginx --format '{{.NetworkSettings.Networks}}'
You'll see it's connected to bridge. Every container on this network gets an IP like 172.17.0.x. They can technically ping each other using these IPs, but here's the catch — they can't use names. The default bridge network has no DNS resolution. So if your app container tries to connect to mysql by name, it'll fail. It has to use something like 172.17.0.3 — which is fragile and terrible for obvious reasons.
Let's see the bridge network in action:
# Run two containers
docker run -d --name container1 alpine sleep 3600
docker run -d --name container2 alpine sleep 3600
# Check their IPs
docker inspect container1 --format '{{.NetworkSettings.IPAddress}}'
docker inspect container2 --format '{{.NetworkSettings.IPAddress}}'
Now get inside container1 and try pinging container2 by name:
docker exec -it container1 ping container2
It'll fail. No DNS. Now try with the actual IP — it'll work. But that's not how you want to build real apps.
Common Mistake: People assume all containers on bridge can talk to each other by name. They can't — not on the default bridge. That's what custom networks solve.
The host network mode tears down the wall between the container and your actual machine's network. The container doesn't get its own IP — it just uses your host machine's IP and ports directly.
docker run --rm --network host nginx
Now Nginx is running directly on port 80 of your actual machine. There's no -p 80:80 needed — because there's no translation happening. The container IS the host, network-wise.
When is this useful? Performance-heavy workloads where the overhead of Docker's network translation (called NAT) matters. Also useful when you're doing network monitoring or you need the container to bind to a very specific interface on the host.
But wait — there's a catch. Host networking only works properly on Linux. On Mac and Windows, Docker actually runs inside a lightweight Linux VM. So when you say --network host, the container is sharing the VM's network, not your Mac's network. This trips up a lot of people.
# On Linux, this would expose port 8080 on the actual machine
docker run --rm --network host -e PORT=8080 my-node-app
# On Mac/Windows, you'd still need -p to access from your browser
Common Mistake: Using host networking on Mac/Windows and wondering why
localhost:8080still doesn't work in the browser. It's not broken — it's just that "host" means the Linux VM, not your Mac.
Sometimes you want a container that does exactly one job and talks to absolutely nobody. Maybe it's a batch processor that just reads from a mounted volume and writes output. No internet, no other containers, nothing.
docker run --rm --network none alpine ping google.com
You'll get something like ping: bad address 'google.com' — because there's no network interface configured at all, except for loopback (lo). The container is completely air-gapped.
This is actually a solid security practice. If a container doesn't need the network, it shouldn't have the network. Less attack surface.
# Check what interfaces exist inside a none-network container
docker run --rm --network none alpine ip addr show
You'll only see lo — the loopback interface. That's it. No eth0, no nothing.
Okay, this is where you go from "Docker beginner" to "Docker practitioner." Custom bridge networks are what you should be using in almost every real-world scenario.
The killer feature? Automatic DNS resolution between containers.
# Create a custom network
docker network create my-app-network
That's it. Now let's run two containers on this network:
docker run -d --name web --network my-app-network nginx
docker run -d --name db --network my-app-network mysql:8 \
-e MYSQL_ROOT_PASSWORD=secret
Now get into the web container and try pinging db by name:
docker exec -it web ping db
It works! Docker has a built-in DNS server (at 127.0.0.11 inside containers) that resolves container names automatically when they're on the same custom network. This is the magic that makes Docker Compose work so smoothly.
# Create a network with a specific subnet
docker network create \
--driver bridge \
--subnet 192.168.100.0/24 \
--gateway 192.168.100.1 \
my-custom-subnet
# List all networks
docker network ls
# Inspect a network to see what's connected
docker network inspect my-app-network
The docker network inspect command is super useful — it shows you every container connected to that network, their IP addresses, and network config. Bookmark that one.
You don't have to decide the network at docker run time. You can attach and detach containers from networks while they're running:
# Connect a running container to a network
docker network connect my-app-network existing-container
# Disconnect it
docker network disconnect my-app-network existing-container
This is handy when you're debugging. Say your app container needs to temporarily talk to a monitoring container on a different network — just connect it, do your thing, disconnect it.
A container can be on multiple networks simultaneously. Think of it like a computer with two network cards — one for internal traffic, one for external.
docker run -d \
--name api-server \
--network frontend-net \
my-api-image
docker network connect backend-net api-server
Now api-server can talk to containers on frontend-net (like the web server) AND containers on backend-net (like the database). It's your network bridge between layers.
The built-in DNS in custom networks resolves containers by their --name. But what if you have multiple containers doing the same thing — say, three identical backend workers? You want to refer to them as a group, not individually.
Enter network aliases.
docker run -d \
--name worker-1 \
--network my-app-network \
--network-alias workers \
my-worker-image
docker run -d \
--name worker-2 \
--network my-app-network \
--network-alias workers \
my-worker-image
docker run -d \
--name worker-3 \
--network my-app-network \
--network-alias workers \
my-worker-image
Now any container on my-app-network that does a DNS lookup for workers gets back all three IPs — and Docker round-robins between them. That's primitive load balancing built right into DNS. No Nginx proxy needed for simple cases.
# Inside another container on the same network
docker exec -it some-other-container nslookup workers
You'll see multiple A records come back — one for each container with that alias.
--link Flag (Don't Use It)You might come across old tutorials using --link to connect containers:
# OLD WAY — please don't do this
docker run --link db:database my-app
--link is deprecated. It was the old manual way to do DNS before custom networks existed. It has weird side effects (it shares environment variables between containers!) and doesn't work with Docker Compose networking at all. Every time you see --link in a tutorial, mentally add "written before 2017" to your notes.
Use custom networks. Always.
Let me walk you through a three-tier app setup using custom networking, because this is what you'll actually build.
# Create separate networks for frontend-backend and backend-db communication
docker network create frontend-net
docker network create backend-net
# Database — only on backend-net
docker run -d \
--name postgres \
--network backend-net \
-e POSTGRES_PASSWORD=mysecret \
postgres:15
# API server — on both networks (it's the bridge)
docker run -d \
--name api \
--network backend-net \
my-api-image
docker network connect frontend-net api
# Frontend — only on frontend-net
docker run -d \
--name frontend \
--network frontend-net \
-p 3000:3000 \
my-frontend-image
Now let's think about what can talk to what:
frontend can reach api by name ✅api can reach postgres by name ✅frontend can NOT reach postgres directly ❌ (different network, no connection)That last point is important. Your frontend is completely isolated from your database at the network level. Even if someone compromised your frontend container, they can't directly hit Postgres. This is the kind of defense-in-depth that makes security folks happy.
When things aren't working, here's your debugging toolkit:
# See all networks
docker network ls
# See which containers are on a network and their IPs
docker network inspect my-app-network
# Check a container's network config
docker inspect container-name --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
# Test DNS resolution from inside a container
docker exec -it my-container nslookup target-container-name
# Test TCP connectivity
docker exec -it my-container nc -zv target-container-name 5432
# Check which port a service is listening on inside a container
docker exec -it my-container ss -tlnp
That nc -zv trick (netcat) is pure gold for debugging. It tells you whether the connection actually reaches the port — if DNS resolves but the port is closed, you'll know it's a service issue, not a network issue.
Unused networks pile up over time. Docker won't let you remove a network that still has containers attached, but you can prune the abandoned ones:
# Remove a specific network
docker network rm my-app-network
# Remove all unused networks
docker network prune
# Nuclear option — removes all stopped containers, unused networks, dangling images
docker system prune
Run docker network ls periodically and you'll notice orphaned networks from old experiments. Clean them up — they don't take much space but they're mental clutter.
| Situation | Network to Use |
|---|---|
| Just testing something quick | bridge (default) |
| Containers need to talk by name | Custom bridge network |
| Max performance on Linux, don't need isolation | host |
| Container should have zero network access | none |
| Multiple services with separate tiers | Multiple custom networks |
| Docker Compose setup | Custom (Compose creates one automatically) |
Using default bridge for multi-container apps — name resolution doesn't work on default bridge, use a custom network.
Expecting --network host to work the same on Mac — it uses the VM's network, not your Mac's. Use -p port mapping instead.
Forgetting to clean up networks — orphaned networks accumulate. Use docker network prune after experiments.
Using --link — it's deprecated. Just stop.
Not segmenting networks in production — your frontend should not be on the same network as your database. Use separate networks and only connect containers to what they need.
Assuming any container can reach any other container — containers on different custom networks are completely isolated by default. You need an explicit docker network connect to bridge them.
Here's a challenge that covers everything in this article. No peeking at the solution until you've tried it yourself.
The Setup:
Build a three-container environment using only Docker CLI commands (no Compose):
app-net and data-netcache on data-netdb on data-net with password challenge123app on app-net that stays running (use sleep infinity)app to data-net as wellapp container, verify:
nslookup cache resolves successfullynslookup db resolves successfullync -zv cache 6379 connects (Redis default port)nc -zv db 5432 connects (Postgres default port)Bonus round:
cache-2 with the network alias cache-clustercache-2 to data-net with the same alias cache-clustercache (add the alias cache-cluster to it too — use docker network connect)app, run nslookup cache-cluster and see that you get back two IP addressesIf you can complete all ten steps, you genuinely understand Docker networking. Not surface-level "I read about it" understanding — actual hands-on "I can debug this in production" understanding.
Docker networking went from "confusing black box" to "oh that makes sense" pretty quickly, right? Here's the one-line summary of everything:
Default bridge is fine for quick tests. Custom networks are for everything real. Host is for performance on Linux. None is for isolation. DNS between containers just works on custom networks — use that, not IPs.
Next up in the series: Docker Compose — where we take everything from this article and define it all in a single YAML file instead of typing out twenty docker run commands. You're going to love it.