Docker User-Defined Networks and DNS
Why containers on the default bridge cannot find each other by name, and how one command fixes it. Build a user-defined network, resolve containers by name and by alias, attach one container to two networks, then cut a network off from the internet entirely.
Storage and Networking Guide 10 of 46 Beginner
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 14 min
- Reviewed20 August 2026
Tested on the versions above. Network IDs and the subnet Docker picks differ on every host. Match the shape of the output, not the exact addresses.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- You can run and remove containers - guide 5 in this path.
- Port publishing and the default bridge - guide 9 in this path.
-
The problem: the default bridge has no DNS
Containers started without a
--networkflag land on the built-inbridgenetwork. They can reach each other by IP address, but Docker runs no DNS resolver there, so names do not work. This is the most common reason a freshly written multi-container setup cannot connect, and the error names an address without ever saying the word network.bash # both containers are on the DEFAULT bridge - name lookup failsdocker exec cg-a ping -c 1 cg-bping: bad address 'cg-b'Expected resultA
bad addresserror, not a timeout. The name never resolved to anything.Success conditionYou can tell the two failures apart.
bad addressis DNS; a hang or100% packet lossafter a name resolves is routing or a firewall instead. -
Create a user-defined network
One command is the whole fix. A user-defined bridge network gets an embedded DNS server that resolves container names automatically. Docker allocates a subnet for you, or you can pin one - useful when you must avoid colliding with a corporate range.
bash Example session docker network create cg-nete15d0235e8d3272f32843320394233582c4e1e4879fd25efb5267c8b1dddbbc8docker network create --driver bridge --subnet 172.31.9.0/24 cg-net26cccdc80ebee9eb1c0eb168e857aad782b2acbafaff4047998d80996c9cfed33Expected resultA long network ID for each.
bridgeis already the default driver, so the second--driverflag is explicit rather than necessary.Success conditionBoth appear in
docker network ls. -
Attach containers, and give one an alias
--networkplaces a container on the network at start time.--network-aliasadds a second name it answers to on that network - the mechanism behind blue/green swaps, and behind Compose letting you reach a service by its service name rather than its container name.bash Example session docker run -d --name cg-web --network cg-net --network-alias api nginx:alpine80d708b8dda854e4105584b2bad17cb07fbf771b36de6065bf30a1ac6947f018docker run -d --name cg-cli --network cg-net alpine:3.22 sleep 300d981c03346bfc24eae222a1c3dc9d82b20b8dff265dbc9ee6a89f79a28cdde05Expected resultTwo container IDs. Nothing is published to the host - these containers talk to each other, not to you.
Success condition
docker pslists both as Up. -
Resolve by name, and by alias
The lookup that failed on the default bridge now works, and the alias resolves to the identical address. Note what is not needed: no published ports, no IP addresses, no editing of
/etc/hosts. Containers on a user-defined network reach each other on the container port directly.bash Example session docker exec cg-cli ping -c 2 cg-webPING cg-web (172.19.0.2): 56 data bytes64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.127 ms2 packets transmitted, 2 packets received, 0% packet lossdocker exec cg-cli ping -c 1 apiPING api (172.19.0.2): 56 data bytesExpected resultBoth names resolve to the same address, and an HTTP request made against the name returns nginx's welcome page.
Verify it worked
bash docker exec cg-cli wget -qO- http://cg-web | head -4<!DOCTYPE html><html><head><title>Welcome to nginx!</title>Success condition
apiandcg-webreturn the same IP. The alias is a real DNS name, not a label. -
See who is on the network
docker network inspectlists the attached containers with their addresses. This is the fastest way to answer "is this container actually on the network I think it is", which is usually the real question hiding behind a connection failure.bash Example session docker network inspect cg-net --format "{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}"cg-web 172.19.0.2/16cg-cli 172.19.0.3/16Expected resultOne line per attached container.
Success conditionBoth containers are listed. A container you expected to see but do not is the bug.
-
A container can join more than one network
docker network connectattaches a running container to an additional network without restarting it, and it gains a second address there. This is how a service sits on both a public-facing network and a private backend network - the arrangement that keeps a database unreachable from anything except the app.bash docker network connect cg-net2 cg-cli# succeeds silentlydocker network disconnect cg-net2 cg-cliExpected resultNo output from either command.
Success conditionBetween the two commands
docker network inspect cg-net2lists cg-cli; afterwards it does not. -
Cut a network off from the internet
--internalcreates a network with no route out. Containers on it still reach each other, but nothing reaches the outside - not even DNS for external names, which is why the failure below is an address error rather than a timeout. For a database tier that is a real containment boundary rather than a convention.bash docker network create --internal cg-isolated1b0042e8981d9a51eff4280618d71d55671c4548892714af66cab0ee9daecf36docker run --rm --network cg-isolated alpine:3.22 wget -qO- --timeout=5 http://example.comwget: bad address 'example.com'Expected resultThe external request fails, exit status 1.
Success conditionThe container cannot reach the internet. Put a second container on this network and the two will still talk to each other.
-
Clean up
Networks are not removed along with the containers that used them, and a network still holding an endpoint refuses to be removed. That is a safety feature rather than an obstacle - remove the containers first.
bash # -f stops and removes these containers immediatelydocker rm -f cg-web cg-clicg-webcg-clidocker network rm cg-net cg-isolatedcg-netcg-isolatedExpected resultEach name echoed back as it is removed.
Success condition
docker network lsshows only the three built-in networks: bridge, host and none.
Troubleshooting
bad address when one container calls another
Why: Both containers are on the default
bridgenetwork, which runs no DNS resolver. Names are never resolved there.Fix:Create a user-defined network and start both containers on it with
--network. Confirm which network each container is really on before changing anything else.bash docker inspect cg-cli --format "{{json .NetworkSettings.Networks}}"# the object keys are the networks this container is attached totemplate parsing error: unexpected "," in range
Why: Docker's
--formatuses Go templates but rejects the two-variable range form{{range $k, $v := ...}}. It exits 64 and prints nothing useful.Fix:Ask for JSON with
{{json ...}}and let a tool that understands JSON iterate, or format a field that is already a list - as the network inspect step above does.bash # this form fails with exit 64:docker inspect NAME --format "{{range $k,$v := .NetworkSettings.Networks}}..."docker inspect NAME --format "{{json .NetworkSettings.Networks}}"network NAME has active endpoints
Why: The network still has a container attached, including stopped ones.
Fix:Remove or disconnect those containers first.
docker network inspectnames exactly which are still attached.bash docker network inspect cg-net --format "{{range .Containers}}{{.Name}}{{println}}{{end}}"Containers resolve each other but the app still cannot connect
Why: DNS is fine and the service is either not listening yet, or is bound to 127.0.0.1 inside its own container - where no other container can reach it.
Fix:Make the service bind 0.0.0.0 rather than localhost, and confirm the port is actually open from the caller before changing anything else.
bash docker exec cg-cli nc -zv cg-web 80