When you run your first container with Docker, networking just works: the process reaches the internet and you publish a port to reach it back. The trouble starts once you run two or three services and find that one cannot see another by name. This guide walks through the network types Docker offers, why the default network falls short, and how to build custom networks that resolve names on their own, with a copy-paste docker-compose.yml.

Key takeaways

  • Docker ships six network drivers: bridge, host, none, overlay, macvlan and ipvlan. On a single-host server, the first three cover almost everything.
  • The default bridge network connects containers by IP in the 172.17.0.0/16 range, but it does not resolve names between them.
  • A user-defined bridge network adds an internal DNS server: containers find each other by name, which is exactly what you need for an app to talk to its database.
  • The host network removes the isolation layer and shares the host’s network stack; the none network leaves the container with no networking at all.
  • Docker Compose creates a user-defined network per project automatically, and every service is reachable by its service name.

What network types does Docker offer?

Docker handles networking through pluggable drivers. A standard install ships six built in, and each one solves a different scenario:

  • bridge: the default driver. It creates a private virtual network on the host and attaches containers to it. This is what you want on a single-host server.
  • host: the container uses the host’s network stack directly, with no NAT or port translation.
  • none: no networking. The container only has its loopback interface.
  • overlay: connects containers spread across several hosts, the foundation of Docker Swarm.
  • macvlan and ipvlan: give each container its own MAC or IP address on your physical network, handy for services that must appear as one more device on the LAN.

You can list them with one command. After installing Docker (if you have not yet, follow the Ubuntu 24.04 install guide), three bootstrap networks already exist:

docker network ls

The output shows bridge, host and none. These three are the ones you will use almost all the time in a homelab or on a VPS.

The default bridge network and its limits

When you launch a container without naming a network, Docker attaches it to the default bridge network, tied to the host’s docker0 interface. Each container gets an IP inside 172.17.0.0/16 and reaches the internet through the 172.17.0.1 gateway via NAT. So far, so good.

The limit shows up with name resolution. On the default network, a container cannot call another by name: it only reaches it by IP, which also changes on every start. The old way to link them was the --link option, now deprecated and discouraged. Running an app and its database on the default network is a reliable source of headaches.

There is a security detail worth remembering. Since Docker Engine v28 (March 2025), the engine drops unsolicited inbound traffic to a container’s internal IP by default unless you have explicitly published the port. In practice, an unpublished port stopped being reachable from other machines on the local network, a hardening step you will appreciate.

User-defined bridge networks and internal DNS

The fix for the naming problem is to create your own bridge network. The difference is large: on a user-defined network, Docker runs an internal DNS server that resolves container names automatically. If you have a container named db, any other container on the same network reaches it by typing db as if it were a domain name.

Creating a network and attaching containers is straightforward:

docker network create --driver bridge my_net
docker run -d --name db --network my_net postgres:17
docker run -d --name web --network my_net nginx:1.29

Now the web container can connect to db:5432 without knowing any IP. Beyond name resolution, a user-defined network improves isolation (only the containers you attach see each other) and lets you connect and disconnect containers on the fly. For any deployment with more than one service, this is the default choice you should make.

The host network and the none network

The host driver removes the middle layer: the container shares the host’s network stack directly. There is no NAT and no published ports, because the container listens on the host’s own ports. It helps a service that needs top network performance or opens many ports, such as a VPN server or a device-discovery tool on the LAN. In exchange, you lose isolation: if the container opens port 8080, it opens it on the host.

One important caveat: the host driver is native on Docker Engine on Linux. On Docker Desktop it only works from version 4.34, must be enabled in settings, operates at layer 4, and only with Linux containers. If you develop on Mac or Windows, keep that in mind before you depend on it.

At the opposite end sits none: the container starts with no network interface other than loopback. Nobody gets in and it does not get out. It suits pure processing tasks that must not touch the network, such as a batch job that only reads and writes to a mounted volume.

How to connect several services in Docker Compose

This is where it all comes together. When you run docker compose up, Docker automatically creates a user-defined bridge network for the project and attaches every service to it. Each service is reachable by its service name, with no manual network creation.

This docker-compose.yml runs a web app and a PostgreSQL database on two segmented networks: the app sits on frontend and backend, the database only on backend, and that internal network has no route to the internet.

services:
  app:
    image: nginx:1.29
    restart: unless-stopped
    ports:
      - "8080:80"
    networks:
      - frontend
      - backend
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: change_this_key
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  frontend:
  backend:
    internal: true

volumes:
  db_data:

Start it with one command and inspect the network it created:

docker compose up -d
docker network inspect $(basename $PWD)_backend

The app reaches the database as db:5432 thanks to the internal DNS. Because db is not on frontend and backend is marked internal, the database stays isolated: it publishes no port and has no route to the internet. Only app publishes port 8080. This pattern of segmenting networks is the foundation for later placing a reverse proxy such as Traefik in front of the app without ever exposing the database.

Publishing ports versus exposing them

This is the most common confusion in Docker networking. Publishing and exposing are not the same thing:

  • Publishing (ports: in Compose, -p HOST:CONTAINER on the command line) maps a container port to a host port. That port becomes reachable from outside Docker. "8080:80" means the container’s port 80 answers on the host’s port 8080.
  • Exposing (expose: in Compose, EXPOSE in the Dockerfile) only documents that the container listens on a port, for internal use between containers. It opens nothing towards the host.

A security nuance: publishing a port binds it to 0.0.0.0 by default, meaning all interfaces. If a service should only be reachable from the server itself (say, an admin panel behind a proxy), publish it locally with 127.0.0.1:8080:80 instead of 8080:80. That keeps the port closed to the network.

Frequently asked questions

Can I reach two containers by name on the default bridge network?

No. The default bridge network has no name resolution, so one container reaches another only by IP, and that IP changes on every start. Create a user-defined bridge network with docker network create and attach both containers to it: then they find each other by name thanks to Docker’s internal DNS.

When should I use the host network instead of bridge?

Use host when the service needs top network performance or opens a lot of ports, such as a VPN or a home-automation system that discovers devices on the LAN. For every other case, a user-defined bridge network is safer because it isolates the container and lets you control which ports you publish.

What is the difference between publishing and exposing a port?

Publishing (-p or ports:) opens the port towards the host and makes it reachable from outside. Exposing (EXPOSE or expose:) is documentation only: it states which port the container listens on so other containers can use it, but it does not open it to the host. To reach a service from your browser, you need to publish it.

Conclusion

Docker networking makes far more sense with one simple rule: the default bridge network is fine for quick single-container tests, but the moment you have two services that must talk to each other, create a user-defined bridge network and let the internal DNS do the work. With Docker Compose that is automatic, and segmenting networks (one public, one internal) gives you isolation for free. The natural next step is data persistence: making what your containers store survive a restart with volumes and bind mounts.

Also available in Spanish: redes en Docker.

Sources

  1. Docker Docs: Networking overview
  2. Docker Docs: Bridge network driver
  3. Docker Blog: Docker Engine 28, hardening container networking
  4. moby/moby on GitHub

Route: Self-hosting with Docker: from zero to production