Docker Networking Basics

Docker networking is how containers talk to each other, to the host machine, and to the outside world. It matters because most useful containers are not isolated forever: a web container needs a database, an API needs Redis, and your browser may need to reach a service running inside a container.

The key idea is simple: containers get their own network namespace, Docker connects them to virtual networks, and port publishing decides what is reachable from the host. Once you understand that localhost changes meaning depending on where a command runs, Docker networking becomes much less mysterious.

Overview: How Docker Networking Works

When Docker starts a container, it creates more than a process from an image. The image supplies read-only filesystem layers, Docker adds a thin writable container layer, and the daemon also prepares runtime isolation such as process, mount, and network namespaces. The network namespace gives the container its own view of network interfaces, routes, and ports. A process inside the container can listen on port 80 without occupying port 80 on the host unless you explicitly publish it.

On a normal single-host Docker Engine, the default network type is a bridge network. A bridge network is a private virtual LAN on the Docker host. Containers attached to the same user-defined bridge network can reach each other directly by container name or network alias through Docker’s embedded DNS server. Containers on different bridge networks are isolated unless you connect them to a shared network or publish ports through the host.

The default built-in bridge named bridge exists for historical compatibility, but user-defined bridge networks are better for real work. They provide automatic DNS by container name, cleaner isolation, and easier lifecycle management. If you run two containers on the same user-defined network, an app can connect to postgres or redis by name instead of hard-coding an IP address. Container IP addresses can change when containers are recreated; names are the stable interface.

Port publishing is separate from container-to-container networking. A container may listen on port 80 internally, but your host cannot reach it through localhost:80 unless Docker installs a published port mapping such as -p 8080:80. That means host port 8080 forwards to container port 80. The Dockerfile instruction EXPOSE is only documentation and image metadata. It does not publish anything by itself.

Docker Desktop on macOS and Windows uses a Linux VM behind the scenes, so low-level bridge interfaces live in that VM rather than directly on your host OS. The user experience is intentionally similar: -p 8080:80 still makes the service reachable at localhost:8080 from your browser. Linux Engine exposes more of the implementation directly, such as bridge devices, iptables or nftables rules, and network namespaces.

Syntax

The common Docker networking command forms are:

docker network COMMAND [OPTIONS]
docker run [OPTIONS] --network NETWORK IMAGE [COMMAND]
docker run -p HOST_PORT:CONTAINER_PORT IMAGE
Command or option Meaning
docker network ls List Docker networks on the current daemon.
docker network create NAME Create a user-defined bridge network by default.
docker network inspect NAME Show driver, subnet, gateway, labels, and connected containers.
docker network rm NAME Remove an unused network.
--network NAME Attach a new container to a network at startup.
docker network connect NAME CONTAINER Attach an existing container to another network.
docker network disconnect NAME CONTAINER Detach a container from a network.
-p 8080:80 Publish host port 8080 to container port 80.
-p 127.0.0.1:8080:80 Bind the published port only to the host loopback interface.

The most common network drivers are bridge, host, none, and overlay. Use bridge for normal single-host applications. host removes much of the network isolation and is mainly a Linux-specific optimization or troubleshooting tool. none gives the container no network access except loopback. overlay is for multi-host networking with orchestration systems such as Swarm.

Examples

Example 1: Create a user-defined bridge network

Create a network, start Nginx on it, and ask a second container to reach Nginx by container name:

docker network create lesson-net
docker run -d --name lesson-web --network lesson-net nginx:1.27-alpine
docker run --rm --network lesson-net curlimages/curl:8.9.1 http://lesson-web

Output:

lesson-net
7d3f4a2c9b10
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

The first command creates a user-defined bridge network. The Nginx container joins that network with the name lesson-web. The curl container joins the same network for one request and resolves lesson-web through Docker’s embedded DNS. No host port is published, so this is private container-to-container communication.

Example 2: Publish a container port to the host

To reach a container from your browser or a host-side tool, publish a port:

docker run -d --name lesson-public -p 8080:80 nginx:1.27-alpine
docker ps --filter name=lesson-public

Output:

f2b6c8a9d123
CONTAINER ID   IMAGE               COMMAND                  STATUS         PORTS
f2b6c8a9d123   nginx:1.27-alpine   "/docker-entrypoint..."   Up 3 seconds   0.0.0.0:8080->80/tcp

Now http://localhost:8080 on the host forwards to port 80 inside the container. The left side of -p is the host port; the right side is the container port. If the host port is already in use, Docker cannot create the mapping and the container start fails.

Example 3: Inspect a network

Inspection shows which containers are attached and what driver Docker is using:

docker network inspect lesson-net

Output:

[
  {
    "Name": "lesson-net",
    "Driver": "bridge",
    "Containers": {
      "7d3f4a2c9b10": {
        "Name": "lesson-web",
        "IPv4Address": "172.20.0.2/16"
      }
    }
  }
]

The exact IP range may differ. Treat the container name, not the IP address, as the stable connection target. Docker may assign a different address after a container is removed and recreated.

Example 4: Let Compose create the network

Compose creates a project network automatically, and service names become DNS names on that network:

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
  checker:
    image: curlimages/curl:8.9.1
    command: ["http://web"]
docker compose up
docker compose down

Output:

[+] Running 3/3
 ✔ Network networking-demo_default      Created
 ✔ Container networking-demo-web-1      Created
 ✔ Container networking-demo-checker-1  Created
checker-1  | <!DOCTYPE html>
checker-1  | <html>
[+] Running 3/3
 ✔ Container networking-demo-checker-1  Removed
 ✔ Container networking-demo-web-1      Removed
 ✔ Network networking-demo_default      Removed

The checker service connects to http://web, not to an IP address. Compose publishes only the web service to the host because only that service has ports:. The checker container can still reach web privately on the project network.

How It Works Step by Step

  1. The Docker CLI sends a create-container request to the Docker daemon with the image, command, network, and port settings.
  2. The daemon prepares the container filesystem from the image’s read-only layers plus a writable layer.
  3. Docker creates or reuses a network namespace for the container and attaches a virtual Ethernet interface to the selected bridge network.
  4. Docker assigns the container an IP address on that network and registers names and aliases with its embedded DNS service.
  5. If you used -p, Docker configures forwarding from the host interface and port to the container IP and port.
  6. The container process starts. When it opens localhost, it sees its own network namespace, not another container and not the host.
  7. When the container exits or is removed, Docker removes its endpoint from the network. User-defined networks remain until you remove them or the Compose project is taken down.

Common Mistakes

Using localhost to reach another container

This Compose fragment is wrong for app-to-Redis communication:

services:
  app:
    image: myapp:1.0
    environment:
      REDIS_HOST: localhost
  redis:
    image: redis:7.2-alpine

Inside the app container, localhost means the app container itself. Use the service name on the shared Compose network:

services:
  app:
    image: myapp:1.0
    environment:
      REDIS_HOST: redis
  redis:
    image: redis:7.2-alpine

Expecting EXPOSE to publish a port

This Dockerfile documents a listening port, but it does not make the app reachable from the host:

FROM nginx:1.27-alpine
EXPOSE 80

Publish a port when starting the container, or use Compose ports::

docker run -d --name exposed-web -p 8080:80 nginx:1.27-alpine

Hard-coding container IP addresses

This is fragile because container IPs are assigned dynamically:

docker run --rm curlimages/curl:8.9.1 http://172.20.0.2

Put both containers on a user-defined network and use the container or service name. Names survive recreation patterns much better than IP addresses.

Publishing private services unnecessarily

A database does not need -p 5432:5432 just so another container can reach it. Publishing exposes a host port; container-to-container traffic on the same network does not require it. Publish only the services that host users or host tools must access.

Best Practices

  • Create a user-defined bridge network for related containers instead of relying on the default bridge network.
  • Use container names, network aliases, or Compose service names for service discovery; avoid hard-coded container IP addresses.
  • Publish only the ports that must be reached from the host or outside the host.
  • Bind sensitive local-only services to loopback with forms such as -p 127.0.0.1:8080:80 when host-only access is enough.
  • Remember that localhost inside a container refers to that container.
  • Use specific image tags such as nginx:1.27-alpine, redis:7.2-alpine, and curlimages/curl:8.9.1 for reproducible examples and deployments.
  • Let Compose manage networks for multi-service local development unless you need to attach services to an existing external network.
  • Use docker network inspect, docker ps, and container logs as your first networking debugging tools.
  • Do not treat EXPOSE as a security control or a publishing mechanism; it is metadata only.

Practice Exercises

  1. Create a network named todo-net. Start an Nginx container named todo-web on it, then use a one-off curl container on the same network to request http://todo-web. Expected end state: curl prints the Nginx welcome page.
  2. Run nginx:1.27-alpine with container port 80 published to host port 8090. Hint: the host port goes on the left side of -p.
  3. Write a compose.yaml with api and redis services. Do not publish Redis. Expected end state: the API configuration points at hostname redis.

Summary

  • Containers have their own network namespaces, so ports inside a container are not automatically host ports.
  • User-defined bridge networks provide private container networking and Docker DNS by name.
  • -p HOST_PORT:CONTAINER_PORT publishes a container port to the host.
  • EXPOSE is documentation and metadata only; it does not publish a port.
  • localhost inside a container means that same container, not another service.
  • Compose automatically creates a project network where service names act as DNS names.
  • Publish fewer ports, use names instead of IPs, and inspect networks when debugging.