Docker Networking Is Confusing Until You Understand This One Thing

by Eric Hanson, Backend Developer at Clean Systems Consulting

The container that can't connect to anything and you don't know why

Your service container starts. It tries to reach a database at localhost:5432. It gets "connection refused." You know the database is running — you can connect to it from your terminal. You add --network host and suddenly it works, but now some other thing breaks. You try 0.0.0.0 as the hostname. Nothing makes sense.

Docker networking has a reputation for being complicated. The actual model is simple once you understand that "localhost" means something different inside a container than it does on your host, and that Docker's networking is built around this distinction.

The one thing: containers have their own network namespace

Each Docker container (by default) runs in its own network namespace. A network namespace is an isolated network stack with its own loopback interface, routing table, and network interfaces.

This means localhost inside a container refers to the container's own loopback interface — not the host's. If your PostgreSQL server is running on the host (not in Docker), localhost:5432 from inside the container doesn't reach it. The container doesn't know the host exists as a network entity unless you explicitly tell it how to reach the host.

Three separate networking questions:

  1. Container to container (within Docker)
  2. Host to container
  3. Container to host or external networks

Each has a different mechanism.

Container to container: bridge networks

When you create a Docker bridge network and attach containers to it, Docker's DNS resolver gives each container a hostname matching its --name or (in Compose) its service name. Containers on the same network reach each other by name.

docker network create app-network

docker run -d --name postgres --network app-network postgres:16-alpine
docker run -d --name app --network app-network \
  -e DB_HOST=postgres \
  my-app:latest

Inside the app container, ping postgres resolves to the postgres container's IP on the bridge network. localhost still refers to the container's own loopback — it doesn't resolve to postgres.

In Docker Compose, this happens automatically. Every service in the same Compose project is added to a shared bridge network, reachable by service name.

services:
  app:
    environment:
      DB_HOST: db     # 'db' is the service name, resolvable within the Compose network

  db:
    image: postgres:16-alpine

The db service name is the DNS hostname within the network. Not localhost, not the container's IP, not 0.0.0.0.

Host to container: port mapping

To reach a container from the host, you publish a port:

docker run -p 8080:80 nginx

This maps host port 8080 to container port 80. A request to localhost:8080 on the host is forwarded to port 80 inside the container. Without -p, the container's ports are not accessible from the host (only from other containers on the same network).

Port mapping creates a NAT rule: host:8080 → container:80. The EXPOSE instruction in the Dockerfile is documentation — it doesn't actually publish the port. Publishing requires -p at runtime.

In Compose:

services:
  app:
    ports:
      - "8080:8080"    # host_port:container_port

Container to host: the host.docker.internal DNS name

If a container needs to reach a service on the host (a database running natively, another service not in Docker), localhost won't work. Use host.docker.internal:

# From inside a container, reach a service on the host at port 5432
psql postgresql://user:password@host.docker.internal:5432/mydb

host.docker.internal is a special DNS name that Docker resolves to the host's gateway IP from the container's perspective. It's available on Docker Desktop (Mac and Windows) by default. On Linux, add it explicitly:

docker run --add-host=host.docker.internal:host-gateway your-image

Or in Compose:

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"

host-gateway is a special Compose string that resolves to the host's IP from the container network.

Network drivers: when the default isn't right

Docker has several network drivers. The two you'll use:

bridge (default): containers on the same bridge network can communicate by name. Containers on different bridge networks cannot communicate unless explicitly connected to both. Port publishing creates NAT rules to the host.

host: the container shares the host's network namespace entirely. No isolation, no port mapping needed, localhost inside the container is the host's localhost. Useful for high-throughput networking where NAT overhead matters. Available on Linux only — Docker Desktop on Mac and Windows doesn't support it.

# With host networking, the container and host share the network stack
docker run --network host nginx
# nginx is now accessible on host port 80 directly, no -p needed

none: no network interfaces except loopback. Container is fully isolated from all external network access. Useful for compute-only workloads that shouldn't have any network access.

The Compose network model in detail

Each Compose project gets a default bridge network named {project-name}_default. All services without an explicit networks: key are added to this network.

You can create additional networks to isolate services from each other:

networks:
  frontend:
  backend:

services:
  proxy:
    networks:
      - frontend

  app:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend    # only reachable by app, not by proxy

The proxy service can reach app (shared frontend network) but not db. The app service can reach both. The db service is isolated to the backend network.

This is the principle of least privilege applied to container networking: services can only talk to services they need to talk to.

Practical debugging

When networking behaves unexpectedly:

# List networks and connected containers
docker network ls
docker network inspect app-network

# Check container's network config
docker inspect container-name | jq '.[0].NetworkSettings'

# Test DNS resolution from inside a container
docker exec -it app-container nslookup db
docker exec -it app-container ping db

# Check if a port is reachable from inside a container
docker exec -it app-container wget -qO- http://db:5432 || echo "unreachable"

# See what ports are exposed and mapped
docker port container-name

The most common issues:

  • Connecting to localhost instead of the service name → fix the URL
  • Service on wrong network → check docker network inspect for connected containers
  • Port not published → add ports: mapping
  • Container to host via localhost → use host.docker.internal

Understanding which of the three flows you're dealing with (container-to-container, host-to-container, container-to-host) determines which fix applies.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Red Flags in a Client Brief That You Should Not Ignore

Some client briefs are invitations to a good engagement. Others are invitations to a difficult one. The difference is usually visible in the brief itself, if you know what to look for.

Read more

No Online System Is Safe? Why Forcing Developers Into the Office Backfires

Managers love to claim “no online system is safe” as a reason to pull developers into the office. But forcing presence often drains lives more than it protects systems.

Read more

How to Stay Visible to Clients Even When You Are Not Working With Them

Being top of mind with past and potential clients does not require constant selling. It requires occasional, genuine presence in their professional orbit.

Read more

How I Give Technical Feedback Without Killing Morale

Technical feedback is necessary, occasionally uncomfortable, and often delivered in ways that make people defensive instead of thoughtful. Here's the approach I've landed on after getting it wrong enough times.

Read more