Docker Homelab
Four white shipping containers on corner platforms wired by blue cables over bridges to a central round hub, evoking a Docker Compose bridge network
networking

Docker Compose Networking: Bridge, DNS, Ports & Aliases

Learn Docker Compose networking from default and external networks to service-name DNS, aliases, port publishing, host mode, macvlan, and common fixes.

By Docker Homelab Editorial · ·Updated August 18, 2026 · 8 min read

Docker networking is where most beginners get confused. Containers seem to work fine until you need two services to talk to each other — then suddenly nothing resolves. This guide explains how Docker Compose networking works from first principles, so you can build stacks that communicate reliably.

The Default Network

When you run docker compose up without specifying any networks, Docker creates a default bridge network for your stack. Every container in the Compose file joins this network automatically.

Within this network, containers can reach each other using their service name as a hostname. If you have a service named db in your Compose file, any other container in the same Compose file can connect to it at db:5432 (or whatever port the database listens on).

This works because Docker injects DNS resolution into each container. The service name resolves to the container’s IP address within the network.

services:
  app:
    image: myapp:latest
    environment:
      # "db" resolves to the db container's IP inside the network
      - DATABASE_URL=postgres://user:pass@db:5432/mydb

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb

The app container can reach the db container at the hostname db. No ports section needed on the db service — the port is only needed if you want to access the database from outside the containers.

Container Name vs Service Name

Two things look similar but behave differently:

  • Service name (db) — the DNS name inside the Docker network. Use this when containers talk to each other.
  • Container name (container_name: my-db) — the name shown in docker ps. You can also use this as a hostname within the same network.

If you don’t set container_name, Docker generates one automatically (like myapp-db-1). The service name always works for inter-container DNS regardless of the container name.

Default Network Isolation

The default network created by docker compose up is isolated per Compose project. Two separate Compose projects can’t reach each other’s services by default, even if they’re on the same host. That shared-network requirement is the single most common cause of a broken reverse proxy setup; see how to set up Traefik with Docker Compose.

This is often what you want — your Nextcloud stack and your Vaultwarden stack shouldn’t need to talk to each other.

When they do need to communicate (or when Traefik needs to route to them), you need a shared external network.

Custom Networks

You can define named networks in your Compose file:

services:
  app:
    image: myapp:latest
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    networks:
      - backend

  nginx:
    image: nginx:alpine
    networks:
      - frontend

networks:
  frontend:
  backend:

In this example:

  • app can reach both db (via backend) and nginx (via frontend)
  • db can only be reached from services on the backend network
  • nginx can only be reached from services on the frontend network

db and nginx cannot communicate directly, even though both are in the same Compose file. This is useful for defense in depth — if a frontend component is compromised, it can’t directly reach the database.

External Networks

An external network is created outside of any Compose file and shared across multiple stacks. This is how Traefik connects to services in separate Compose projects:

# Create once
docker network create traefik-proxy

Then reference it as external in each Compose file:

services:
  myservice:
    image: myimage:latest
    networks:
      - traefik-proxy

networks:
  traefik-proxy:
    external: true

The external: true tells Docker Compose “this network already exists — don’t try to create or delete it.”

Without external: true, Compose would try to create a network named traefik-proxy scoped to this project, which would be a different network than the one Traefik is watching.

Creating a Network by Hand

docker network create takes options that a Compose-managed network cannot easily be given after the fact, which is the main reason to create one up front:

# Plain user-defined bridge, the usual case
docker network create traefik-proxy

# With a fixed subnet, so container addresses are predictable
docker network create --subnet 172.28.0.0/16 homelab

# Confirm what exists and who is attached
docker network ls
docker network inspect traefik-proxy

docker network inspect is the fastest way to answer “why can’t these two containers see each other” — its Containers block lists every attached container with the name Docker’s DNS has registered for it. If the container you expect is not in that list, the problem is attachment, not DNS.

A network created this way outlives docker compose down, which is exactly what you want for a shared proxy network. Compose-created networks are removed with the project.

Naming: Project Prefixes and the name Key

Compose prefixes the networks it creates with the project name, so a network declared as backend in a project called media is actually created as media_backend. That prefix is why an external: true reference has to use the real name, not the short one.

To pin a Compose-managed network to an exact name with no prefix, use the name key:

networks:
  backend:
    name: homelab-backend   # created exactly as written, no project prefix

Network Aliases

A service is reachable by its service name automatically. An alias adds extra DNS names for the same container on a given network:

services:
  db:
    image: postgres:16
    networks:
      backend:
        aliases:
          - postgres
          - database

networks:
  backend:

Other containers on backend can now reach it as db, postgres, or database. Two situations make this genuinely useful rather than cosmetic.

The first is migration: an application with a hardcoded hostname from an older setup keeps working if you alias the new service to the old name, with no change to the application’s configuration.

The second is per-network identity. A container attached to two networks can carry a different alias on each, which lets a frontend and a backend refer to the same service by different names:

services:
  api:
    image: myapi:latest
    networks:
      frontend:
        aliases:
          - api-public
      backend:
        aliases:
          - api-internal

Aliases are not unique. Several containers can share one alias on the same network, in which case Docker’s DNS returns all of their addresses and the client picks one — a crude round-robin that is occasionally useful for a stateless service and is a source of confusion for everything else.

links predates user-defined networks. It was the original way to let one container reach another, and it worked by injecting /etc/hosts entries and environment variables into the dependent container:

services:
  app:
    image: myapp:latest
    links:
      - db          # legacy - do not use in new files

On any modern Compose file it is redundant. Every service in a project already shares a default network with automatic DNS, so db resolves without links doing anything. Three reasons to remove it when you inherit it:

  • It cannot be combined with network_mode: host. The engine refuses to create the container, with conflicting options: host type networking can't be used with links. This would result in undefined behavior.
  • It implies a start-order dependency that people mistake for a readiness guarantee. It is not one; use depends_on with a healthcheck condition, as shown at the end of this guide.
  • The alias form (links: ["db:database"]) is better expressed as a network alias, which works consistently across all attached services rather than only the linked one.

If you need the alias behaviour, use the aliases key above. If you need ordering, use depends_on. There is no remaining case for links in a new Compose file.

Ports vs No Ports

services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"  # exposes to the host — accessible from your LAN

vs

services:
  db:
    image: postgres:16
    # no ports — only reachable from containers on the same Docker network

The ports: section publishes the container port to the host machine, making it accessible from outside Docker — your LAN, or the internet if you have port forwarding.

Without ports:, the service is only reachable from other containers on the same Docker network. This is what you want for databases, caches, and anything that shouldn’t be directly accessible from outside.

Rule of thumb: only publish ports for services that need to be accessed directly from outside Docker. Let everything else communicate via Docker networks.

Beyond Bridge: Host and Macvlan

Two other modes exist for the cases a bridge network cannot serve, and both trade isolation for direct access to your LAN.

network_mode: host skips Docker networking entirely and gives the container the host’s network stack:

services:
  pihole:
    image: pihole/pihole:latest
    network_mode: host

This is what you need for services that must see LAN broadcast or multicast traffic — Pi-hole serving DHCP, Home Assistant discovering devices — because NAT destroys both. The costs are real, though: ports: entries are silently ignored, service-name DNS stops resolving, reverse-proxy discovery breaks, and port conflicts become host-wide. The Docker Compose host networking guide works through each of those and the errors they produce.

A macvlan network instead gives each container its own MAC address and its own IP on your LAN, so it appears as a separate device rather than borrowing the host’s identity:

networks:
  lan:
    driver: macvlan
    driver_opts:
      parent: eth0
    ipam:
      config:
        - subnet: 192.168.1.0/24
          gateway: 192.168.1.1
          ip_range: 192.168.1.240/28

That solves the port-conflict problem completely — every container owns all the ports on its own address — at the cost of one surprising rule: the Docker host cannot reach its own macvlan containers without an extra shim interface. Setup, the shim, and the VLAN-tagged variant are covered in the Docker macvlan networking guide.

For most services, neither is needed. A bridge network with published ports keeps isolation, DNS, and automatic proxy routing all working, and that combination is worth giving up only when a specific protocol demands it. The Pi-hole decision, which is the one homelab case where all three modes are defensible, is worked through in how to set up Pi-hole in Docker Compose.

Common Problems and Fixes

“Service ‘db’ not found” or connection refused between containers:

Check that both containers are on the same network. Services in different Compose projects are on different default networks and can’t reach each other unless you add them to a shared external network.

“Name or service not known” when using the service name:

Verify the service name in the YAML matches what you’re connecting to. Check with docker network inspect <network-name> to see which containers are on the network and what names they’re registered with.

Port conflict on the host:

Two containers trying to publish the same host port. Change one of the host ports (the left side of the host:container mapping). Container ports (right side) don’t conflict — they’re inside separate namespaces. Which is why a proxy lets you drop published ports entirely; see the Nginx Proxy Manager tutorial.

Container exits immediately, can’t connect to db:

Database containers take a few seconds to initialize. Your app may be starting before the database is ready. Add depends_on with a healthcheck condition to make the app wait.

services:
  app:
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

Where to Go Next

Docker networks control what containers can reach inside the host. They say nothing about what reaches the host in the first place — that boundary belongs to your router and firewall, which the homelab firewall comparison at firewallcompare.com covers.

Inside the host, the next step is usually a reverse proxy, so that a single published port serves every service by hostname instead of a growing list of port numbers. Start with the Traefik with Docker Compose guide for label-driven routing, or the Nginx Proxy Manager tutorial if you would rather configure it through a web UI. To generate a stack with the networks and proxy already wired together, the interactive compose stack builder produces a single valid docker-compose.yml from a list of services.

Related