You spin up a container. It runs fine. Then you spin up a second one — and they can’t talk. No pings. No database connections. Just silence.

Server rack with networking cables
Image: Federal Bureau of Investigation via Wikimedia Commons (Public Domain)

I’ve hit that wall more times than I care to admit. The first time it happened, I was trying to connect a Python app container to a PostgreSQL container. Everything looked right — the database was running, the credentials were correct — but the app just timed out. I spent an hour debugging connection strings before realizing: the containers weren’t on the same network.

Docker networking isn’t complicated once you understand the mental model. But the official docs throw you into the deep end with overlay networks, ingress routing, and Swarm mode before you’ve even grasped the basics. Let’s fix that.

The Default Setup: What Docker Gives You for Free

When Docker starts on your machine, it creates three networks automatically:

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
a1b2c3d4e5f6   bridge    bridge    local
g7h8i9j0k1l2   host      host      local
m3n4o5p6q7r8   none      null      local

The bridge network is the default. Every container you run without specifying a network lands here. Docker assigns each container an internal IP (usually in the 172.17.0.0/16 range) and they can communicate with each other — but only by IP address, not by container name.

That last part trips people up. Two containers on the default bridge can ping each other by IP, but DNS resolution (using container names like my-postgres-db) doesn’t work. You need a user-defined bridge network for that.

Custom Bridge Networks: The Real Way to Connect Containers

Create a custom bridge network and DNS just works. Container names become hostnames.

$ docker network create my-app-net

Now run two containers on this network:

$ docker run -d --name db --network my-app-net     -e POSTGRES_PASSWORD=secret postgres:16

$ docker run -it --name app --network my-app-net     alpine ping db

The Alpine container resolves db to the PostgreSQL container’s IP automatically. No hardcoded IPs, no manual hosts file entries. Docker’s embedded DNS server handles it.

Here’s a quick way to inspect what’s happening under the hood:

$ docker network inspect my-app-net

You’ll see every container attached, their IPs, and the subnet configuration. This command has saved me countless headaches when debugging why two services won’t connect — it’s the first thing I run when something goes quiet.

Port Mapping: Letting the Outside World In

Internal container-to-container communication is one thing. But what about traffic from your browser, your API client, or another machine on your network? That’s where port mapping comes in.

$ docker run -d --name web --network my-app-net     -p 8080:80 nginx:alpine

This maps port 8080 on your host machine to port 80 inside the container. Now http://localhost:8080 reaches Nginx. The -p flag accepts host_port:container_port — you can also bind to a specific interface with 127.0.0.1:8080:80 if you don’t want the service exposed on all network interfaces.

One thing I learned the hard way: if you forget the port mapping on a container that’s already running, you can’t add it. You have to stop, remove, and recreate the container. Docker Compose makes this less painful (we’ll get to that).

Docker Compose Networking: No Port Mapping Headaches

Docker Compose automatically creates a network for your services. Every service can reach every other service by its service name — no explicit network configuration needed.

# docker-compose.yml
version: '3.8'
services:
  api:
    image: my-python-api:latest
    ports:
      - "3000:3000"
    environment:
      DB_HOST: db
      DB_NAME: myapp
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: secret

The API container connects to the database using the hostname db — Compose handles the DNS. This is why I mentioned Docker Compose for local development earlier: it eliminates the networking guesswork completely.

But Compose networks are isolated by default. Two separate Compose projects can’t talk to each other unless you explicitly attach them to the same external network:

# docker-compose.yml (project A)
networks:
  shared:
    external: true
    name: shared-network

This is useful when you have microservices in different repos that still need to communicate during local development.

Network Drivers: Bridge, Host, and When to Use Each

Docker ships with several network drivers. For local development, you’ll mostly deal with three:

Driver Behavior Use Case
bridge Isolated network with NAT Default for standalone containers
host Container shares host network stack Maximum performance, no port mapping needed
none No networking at all Security isolation or batch jobs

The host driver is interesting. It removes network isolation entirely — the container uses the host’s interfaces directly. This is faster (no NAT overhead) but less secure. It’s useful for high-throughput services or when you’re running a tool that needs to listen on many ports without mapping each one.

$ docker run --network host nginx
# Nginx is now directly accessible on port 80 of the host

Most of the time, stick with bridge networks. Host networking is a performance optimization you reach for when profiling shows NAT overhead is actually your bottleneck.

Troubleshooting: The Three Commands I Use Daily

When containers won’t talk, here’s my debugging checklist:

1. Is the container actually on the network you think it is?

$ docker inspect <container> | grep -A 10 NetworkSettings

Look for the Networks section — it lists every network the container is attached to.

2. Can the containers reach each other at all?

$ docker exec <container> ping <other-container-name>

If DNS resolution works but ping doesn’t, it’s a firewall or application-level issue. If DNS fails, check that they’re on the same user-defined bridge network.

3. Is the port actually listening inside the container?

$ docker exec <container> netstat -tlnp
# or if netstat isn't available:
$ docker exec <container> ss -tlnp

If the service is listening on 127.0.0.1 instead of 0.0.0.0, it won’t accept connections from other containers. This is a surprisingly common issue — some frameworks default to localhost-only binding for security, which breaks container networking.

Speaking of debugging, if you’re hitting HTTP APIs from containers, knowing your way around curl inside a container saves a lot of guesswork.

When to Reach for Something Bigger

Docker’s built-in networking works beautifully for development and single-host deployments. But once you’re orchestrating containers across multiple machines, you hit its limits. That’s where Kubernetes enters the picture — but for most small teams, it’s overkill. I wrote about why I stopped recommending Kubernetes to small teams — the short version is that Docker Compose plus a solid CI/CD pipeline handles 90% of use cases without the operational complexity.

And if you’re exposing containerized services to the internet, you’ll want a reverse proxy in front. Setting up Nginx as a reverse proxy is the natural next step after you’ve got your containers talking to each other.

The Bottom Line

Docker networking clicked for me when I stopped thinking about it as a network administrator’s problem and started thinking about it as a developer’s tool. User-defined bridge networks give you DNS-based service discovery for free. Docker Compose networks eliminate the boilerplate. The host driver is there when you need raw speed.

Most of the problems I see come from two things: using the default bridge instead of a custom one (no DNS), or binding services to localhost instead of all interfaces (no external connectivity). Fix those two, and Docker networking stops being a mystery.

Now go build something that talks to itself.

Filed under Tech & Gadgets
Last Update: June 20, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted