User-Defined Networks
Summary
- A user-defined network is a bridge you create yourself, giving containers automatic name-based DNS and stronger isolation than the default bridge.
- Insight: The single biggest reason to create one is DNS - containers resolve each other by name, so you never hardcode IPs.
- Key Concepts: user-defined bridge, embedded DNS (127.0.0.11), service discovery, network aliases, Compose default network.
- When to Use: Any multi-container app - a web tier talking to an API talking to a database.
- Limitations: DNS and IP allocation are single-host; cross-host discovery needs an overlay network or Kubernetes Services.
- Related: Docker networking modes, how containers get networking, best practices.
Recipe
Create a network, attach containers, and reach them by name.
docker network create appnet
docker run -d --network appnet --name db postgres:16
docker run -d --network appnet --name api myapi:1.0
docker exec api getent hosts dbThe api container resolves db to its current IP through Docker's embedded DNS.
No published ports are needed for container-to-container traffic on the same network.
Working Example
In practice you declare this in Compose, which creates a user-defined network for the project automatically.
services:
api:
image: myapi:1.0
environment:
DATABASE_URL: "postgres://app:secret@db:5432/app"
depends_on:
db:
condition: service_healthy
networks:
- backend
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
networks:
- backend
networks:
backend:The API connects to the host db - the service name - not to an IP address.
docker compose up -d
docker compose exec api ping -c1 dbBecause both services are on the backend network, db resolves and no port needs publishing.
Deep Dive
How the DNS actually resolves
Every container on a user-defined network gets /etc/resolv.conf pointed at 127.0.0.11.
That address is Docker's embedded DNS server, which answers with the current IP of any container name or network alias on the same network.
This is why a restarted container that gets a new IP is still reachable by name - the DNS record follows the name, not the address.
The default bridge network deliberately lacks this; it only supports the legacy --link flag, which you should not use.
Service names versus network aliases
In Compose, the service key (db, api) is the DNS name.
You can add extra names with aliases, useful when several services should answer to one logical name.
services:
primary:
image: postgres:16
networks:
backend:
aliases:
- dbNow both primary and db resolve to the same container.
Multiple networks for segmentation
Attaching a service to more than one network lets you split public and private traffic.
services:
web:
image: nginx:1.27
networks: [frontend, backend]
db:
image: postgres:16
networks: [backend]Here db is unreachable from anything on frontend alone, which is a simple, effective isolation boundary.
Subnet and IP address management
When you create a network, Docker's default IPAM driver allocates a subnet from a private pool.
You can pin it if you need to avoid overlap with an existing corporate range.
docker network create --subnet 172.28.0.0/16 --gateway 172.28.0.1 appnetPinning is useful in tightly managed environments, but for most apps the default allocation is fine and DNS makes the exact IPs irrelevant.
Gotchas
Container names must be unique per network for DNS to work; two containers cannot share a name.
The default bridge network gives no DNS, so docker run without --network will leave you unable to resolve names - always create or use a user-defined network.
DNS caching in some language runtimes pins the first resolved IP; if a dependency restarts and changes IP, long-lived clients may keep hitting the old address until they re-resolve.
depends_on controls start order, not readiness - use condition: service_healthy with a healthcheck so the API waits for the database to actually accept connections.
Compose prefixes networks with the project name, so backend becomes myproject_backend; reference it by the short name inside the file, but expect the prefixed name in docker network ls.
Alternatives
Default bridge with --link: deprecated and single-direction; avoid it entirely in favor of user-defined bridges.
Host networking: removes isolation and DNS but gives raw host performance; only for single, trusted, high-throughput containers.
Overlay networks: the multi-host answer for Docker Swarm, extending DNS-based discovery across nodes via VXLAN.
Kubernetes Services: the cluster-scale equivalent - a stable virtual IP and cluster DNS name in front of ephemeral Pods, which is the right tool once you move beyond one host.
FAQs
Do I need to publish ports for containers to talk to each other? No. Publishing is only for reaching a container from outside the host; same-network containers talk directly.
Why does name resolution fail on the default bridge? The default bridge has no embedded DNS. Create a user-defined network and DNS works automatically.
Can two Compose projects share a network?
Yes, by declaring an external: true network that both projects reference by its real name.
What IP does the embedded DNS server use?
127.0.0.11 inside each container on a user-defined network.
Does this work across multiple hosts? Not with a plain bridge. Use an overlay network (Swarm) or Kubernetes Services for cross-host discovery.
Related
- How Containers Get Networking and Storage
- Docker Networking Basics
- Volumes & Bind Mounts
- Networking & Storage Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).