Intro
Docker networking is one of the most common sources of confusion when moving from a single container to multi-container applications. Misconfigured networks lead to containers that cannot reach each other, DNS resolution failures, or unintended exposure to the host. Rather than experimenting on a production host, you can build a safe local Docker networking lab to test configurations, troubleshoot behavior, and create repeatable learning workflows. A controlled lab helps you isolate problems, verify fixes, and build confidence before making changes elsewhere in your infrastructure.
This guide walks through setting up such a lab with concrete commands and expected outputs. We will create dedicated Docker bridge networks, launch containers with static IP addresses and custom hostnames, test connectivity and DNS resolution, demonstrate network isolation, and then examine common failure modes along with recovery steps. All examples are designed to run on a single Linux host with Docker installed, including Docker Desktop on Windows or macOS. The commands are safe for your local environment and do not affect other Docker networks or running containers.
A local lab is especially valuable when you are learning Docker networking concepts or troubleshooting an issue you suspect is network-related. Instead of guessing or reading through lengthy documentation, you can reproduce the problem in minutes, test fixes, and document what works. The ability to inspect network internals directly inside containers gives you immediate insight into how Docker manages networking under the hood.
Version and Environment Inventory
Before starting, confirm your environment meets the following prerequisites:
- Docker Engine version 20.10 or later, or Docker Desktop equivalent.
- A Linux host (native or virtual machine) with a Bash shell. Windows Subsystem for Linux 2 (WSL2) works well, as does Docker Desktop with Linux containers.
- Basic familiarity with the Docker CLI and container lifecycle commands such as
docker run,docker exec, anddocker rm. - No existing user-defined networks named
labnet1orlabnet2, and no containers namedlab1,lab2, orlab3(we will create them).
To check your Docker version, run:
docker version --format '{{.Server.Version}}'
Expected output similar to:
20.10.21
Also confirm you can list current networks:
docker network ls
You should see output similar to the following (network IDs will vary):
NETWORK ID NAME DRIVER SCOPE
abc123def456 bridge bridge local
ghi789jkl012 host host local
mno345pqr678 none null local
If you see the default bridge, host, and none networks, you are ready to proceed. This lab uses user-defined bridge networks, which provide automatic DNS resolution between containers and isolation from the host network stack.
Safe Configuration Path
We will build the lab incrementally, verifying each step before moving to the next. This approach limits the scope of changes and makes rollback straightforward: if something goes wrong, you can remove only the resources you just created without affecting the rest of your environment.
Step 1: Create an isolated user-defined bridge network
Create a network named labnet1 with a specific subnet to avoid conflicts with existing networks and to allow static IP assignments:
docker network create \
--driver bridge \
--subnet 192.168.100.0/24 \
--gateway 192.168.100.1 \
labnet1
Expected output is the network ID, for example:
6f8d4c9b2a1e
Verify the network exists and has the correct configuration:
docker network inspect labnet1 --format '{{.Name}} {{.Driver}} {{.IPAM.Config}}'
Expected output:
labnet1 bridge [{192.168.100.0/24 192.168.100.1 map[]}]
This confirms that the network is a bridge driver with the specified subnet and gateway.
Step 2: Launch two containers on the network with static IPs
We will run two Alpine Linux containers because they are lightweight and start quickly. Each container gets a static IP address and a custom hostname. Start the first container, lab1, with IP 192.168.100.10:
docker run -dit \
--name lab1 \
--network labnet1 \
--ip 192.168.100.10 \
--hostname lab1 \
alpine:latest
Expected output is the container ID, for example:
abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
Start the second container, lab2, with IP 192.168.100.20:
docker run -dit \
--name lab2 \
--network labnet1 \
--ip 192.168.100.20 \
--hostname lab2 \
alpine:latest
Verify both containers are running:
docker ps --filter "name=lab" --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
Expected output:
NAMES IMAGE STATUS
lab1 alpine:latest Up 10 seconds
lab2 alpine:latest Up 8 seconds
Step 3: Test basic connectivity between containers
Execute a ping from lab1 to lab2 using its IP address:
docker exec lab1 ping -c 2 192.168.100.20
Expected output (timing values may vary):
PING 192.168.100.20 (192.168.100.20): 56 data bytes
64 bytes from 192.168.100.20: seq=0 ttl=64 time=0.123 ms
64 bytes from 192.168.100.20: seq=1 ttl=64 time=0.098 ms
--- 192.168.100.20 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.098/0.110/0.123 ms
Now test using the container name to verify DNS resolution:
docker exec lab1 ping -c 2 lab2
Expected output should be similar to the previous ping, showing that lab2 resolved to 192.168.100.20. This confirms that Docker's embedded DNS server works automatically on user-defined networks.
Step 4: Expose a simple service and test from another container
Testing at the network layer is useful, but a practical lab should also verify service-level connectivity. We will run a lightweight web server in lab2 using Python's built-in HTTP server. Alpine Linux does not include Python by default, so first install it inside lab2:
docker exec lab2 apk add --no-cache python3
Then start a simple HTTP server on port 8000 inside lab2:
docker exec -d lab2 python3 -m http.server 8000
The -d flag runs the server in detached mode inside the container. Verify the server is listening:
docker exec lab2 netstat -tuln | grep 8000
Alternatively, use ss -tuln if netstat is not available. Expected output shows a line with 0.0.0.0:8000 indicating the server is listening on all interfaces.
Now test from lab1 using wget (available in Alpine by default) to fetch a file from the server. Since the server's root directory is / inside lab2, you can fetch /etc/hostname to see the hostname:
docker exec lab1 wget -qO- http://192.168.100.20:8000/etc/hostname
Expected output:
lab2
You can also use the container name in the URL to test DNS-based service discovery:
docker exec lab1 wget -qO- http://lab2:8000/etc/hostname
This should produce the same output, confirming that the embedded DNS resolves lab2 and that the HTTP service is reachable.
Step 5: Create a second network to demonstrate isolation
Create another network labnet2 with a different subnet:
docker network create \
--driver bridge \
--subnet 10.10.10.0/24 \
--gateway 10.10.10.1 \
labnet2
Attach a new container lab3 to labnet2:
docker run -dit \
--name lab3 \
--network labnet2 \
--ip 10.10.10.10 \
--hostname lab3 \
alpine:latest
Now test connectivity from lab1 (on labnet1) to lab3 (on labnet2) by IP address:
docker exec lab1 ping -c 2 10.10.10.10
Expected output will show 100% packet loss, because the two networks are isolated unless you explicitly connect them. This demonstrates Docker network isolation: containers on different user-defined bridge networks cannot communicate directly, even if they know each other's IP addresses.
Step 6: Connect a container to multiple networks
If needed, you can connect lab1 to labnet2 as well, allowing it to communicate with lab3. This is a common pattern for multi-tier applications where a container needs to access services on different networks.
docker network connect labnet2 lab1
After connecting, lab1 receives an additional IP address on labnet2. Docker automatically assigns the next available IP in that subnet, for example 10.10.10.2. Verify by inspecting the network interfaces inside lab1:
docker exec lab1 ip addr show
Look for an interface (likely eth1) with an IP address in the 10.10.10.0/24 subnet. Then test ping again from lab1 to lab3:
docker exec lab1 ping -c 2 10.10.10.10
Now it should succeed, showing connectivity after attaching to the same network. This demonstrates how Docker allows containers to be part of multiple networks, which is useful for controlled communication between different tiers.
Verification and Diagnostics
Proper verification ensures your lab behaves as expected. Use the following commands to inspect network configuration and connectivity.
Inspect network details
docker network inspect labnet1
This returns a JSON object with the subnet, gateway, containers attached, and their IP addresses. For a concise view, use:
docker network inspect labnet1 --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'
Expected output:
lab1 192.168.100.10/24
lab2 192.168.100.20/24
This confirms IP assignments.
Check DNS resolution
From lab1, resolve the hostname lab2 using nslookup (available via BusyBox in Alpine):
docker exec lab1 nslookup lab2
Expected output similar to:
Server: 127.0.0.11
Address: 127.0.0.11:53
Name: lab2
Address: 192.168.100.20
The DNS server at 127.0.0.11 is Docker's embedded DNS, which runs inside each container on user-defined networks.
Verify isolation
Check that containers on separate networks cannot reach each other without explicit connection. From lab1 (if not connected to labnet2), ping lab3's IP and confirm failure. A successful ping after connecting demonstrates the change.
Examine container network interfaces
docker exec lab1 ip addr show
Look for the eth0 interface with the assigned IP. You can also check routing:
docker exec lab1 ip route
Expected output shows the default gateway via 192.168.100.1, along with the subnet route.
Common Pitfalls and How to Avoid Them
Even in a controlled lab, certain mistakes are common. Recognizing them early can save time and frustration.
Pitfall 1: Using the default bridge network
Many beginners place containers on the default bridge network and then wonder why DNS resolution fails. The default bridge network does not provide automatic DNS resolution between containers; you must use legacy --link flags (deprecated) or, better, create a user-defined bridge network. Why it happens: Docker's default bridge is designed for backward compatibility and does not have the embedded DNS server or automatic service discovery that user-defined networks provide. How to avoid: Always create a dedicated user-defined network for your applications. This lab demonstrates the correct approach.
Pitfall 2: Static IP conflicts
Assigning a static IP that is already in use by another container on the same network causes an error such as Address already in use. Why it happens: Docker tracks IP allocations in its IPAM (IP Address Management) database, and each IP on a subnet must be unique. How to avoid: Before assigning a static IP, inspect the network to see existing allocations using docker network inspect. Alternatively, use a subnet with enough addresses and plan your IP scheme. If a conflict occurs, choose a different IP or remove the conflicting container.
Pitfall 3: Forgetting to connect a container to the second network
When testing multi-network connectivity, it is easy to forget that a container is not automatically attached to all networks. Pinging across networks will fail until you explicitly connect the container. Why it happens: Docker networks are isolated by default, and containers only join networks they are explicitly connected to. How to avoid: Always verify network membership with docker network inspect before expecting cross-network communication. If needed, use docker network connect as shown in Step 6.
Pitfall 4: Misinterpreting DNS resolution failures
If ping lab2 returns Name or service not known, it is tempting to think Docker's DNS is broken. Why it happens: Usually, the containers are on the default bridge network, or the container name is misspelled, or the containers are on different user-defined networks. How to avoid: Check the network membership of both containers with docker network inspect. Ensure they are on the same user-defined network. If not, connect them or create a new network and attach both.
Pitfall 5: Leaving test containers and networks running
After experimenting, you may forget to clean up, leaving orphaned containers and networks that consume resources and clutter your environment. Why it happens: It is easy to move on to the next task without deleting lab resources. How to avoid: Adopt a habit of tearing down the lab when finished, as described in the cleanup section below. You can also use labels or naming conventions to identify lab resources easily.
Failure Modes and Recovery
Understanding common failure modes helps you recover quickly and keeps your lab productive.
IP address conflict
If you attempt to assign a static IP already in use, Docker returns an error like:
Error response from daemon: Address already in use
Recovery: Choose a different IP or inspect existing allocations with docker network inspect labnet1 to find the conflict. Remove the conflicting container if necessary and retry.
Container cannot resolve DNS names
If DNS resolution fails (e.g., ping lab2 gives Name or service not known), check if the containers are on the same user-defined network. DNS resolution only works on user-defined networks, not the default bridge. Verify with docker network inspect and ensure both containers are attached.
Recovery: Connect both containers to the same user-defined network using docker network connect.
Network isolation breaks unexpectedly
If containers that should be isolated suddenly can communicate, check if they are connected to a common network (e.g., both attached to the same bridge). Use docker network inspect to review connections.
Recovery: Disconnect the unintended network connection with docker network disconnect <network> <container>.
Container loses connectivity after network changes
Modifying network settings (e.g., disconnecting a container) can disrupt active connections.
Recovery: Reconnect the container to the network if needed, or restart the container with docker restart <container> to re-establish networking. In some cases, recreating the container may be necessary.
Port publishing misconfiguration
If you expose a service inside a container but cannot reach it from the host, you may have forgotten to publish the port with -p. Without port publishing, the service is only accessible from other containers on the same network.
Recovery: Re-run the container with the appropriate -p flag, or use docker network connect to allow access from another container on the same network without exposing it to the host.
Rollback strategy
Because the lab is isolated, rollback often means removing the test networks and containers. Save your setup commands in a script for reproducibility. To tear down everything:
docker rm -f lab1 lab2 lab3
docker network rm labnet1 labnet2
Verify removal with docker network ls and docker ps -a.
Operations Checklist
Use this checklist to run the lab reliably and extend it safely. Each step includes a command or check and the expected outcome. For each major action, assign an owner (yourself in a personal lab, or a specific team member in a shared environment) and review the results before proceeding. In a team setting, revisit the lab setup monthly to ensure it still meets current needs and that no conflicts have arisen.
| Step | Action | Command / Check | Owner | Review Frequency |
|---|---|---|---|---|
| 1 | Verify Docker is running | docker info | Lab operator (e.g., Alex) | At each lab session |
| 2 | List existing networks | docker network ls (ensure no conflict with lab names) | Lab operator | At each lab session |
| 3 | Create lab network with subnet | docker network create --subnet 192.168.100.0/24 --gateway 192.168.100.1 labnet1 | Lab operator | At each lab build |
| 4 | Launch test containers with static IPs | docker run -dit --name lab1 --network labnet1 --ip 192.168.100.10 alpine:latest (repeat for lab2) | Lab operator | At each lab build |
| 5 | Test IP connectivity | docker exec lab1 ping -c 2 192.168.100.20 (expect 0% loss) | Lab operator | After container start |
| 6 | Test DNS-based connectivity | docker exec lab1 ping -c 2 lab2 (expect success) | Lab operator | After container start |
| 7 | Verify isolation | Ping 10.10.10.10 from lab1 before connecting (expect failure) | Lab operator | When testing isolation |
| 8 | Connect to additional network if needed | docker network connect labnet2 lab1 | Lab operator | When multi-network needed |
| 9 | Inspect network configuration | docker network inspect labnet1 --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}' | Lab operator | After any network change |
| 10 | Clean up when done | docker rm -f lab1 lab2 lab3 && docker network rm labnet1 labnet2 | Lab operator | End of session |
Repeat these steps as needed for experiments. Document any variations and their outcomes for future reference. Maintaining a lab journal helps you track what you have tested and the results, making it easier to reproduce or share findings with your team.
Conclusion
A local Docker networking lab provides a safe, repeatable environment to learn and test network configurations. In this guide, we created isolated bridge networks, deployed containers with static IPs and hostnames, verified connectivity and DNS, demonstrated network isolation, and discussed common failure modes with recovery steps. By following the operations checklist and avoiding the pitfalls described, you can extend the lab to test more complex scenarios such as overlay networks, network policies, or multi-host setups.
Start with a narrow pilot network, inspect behavior locally, and then apply verified changes to your development or production environments with confidence. The skills you build in this lab will transfer directly to real-world Docker deployments, helping you troubleshoot networking issues faster and design more robust container architectures.