Tools: Docker Networking Explained: Connect Your Containers (2026)

Tools: Docker Networking Explained: Connect Your Containers (2026)

🌐 Docker Networking Explained: Connect Your Containers (2026)

πŸ€” Why This Matters

βœ… Prerequisites

πŸ—οΈ The Default Bridge Network

🌐 Port Mapping: Expose Containers to the Host

πŸŒ‰ Custom Bridge Networks: The Right Way

πŸ§ͺ Exercise: CloudBeaver + PostgreSQL on a Custom Network

Part 1: Create the Network and Run PostgreSQL

Part 2: Run CloudBeaver on the Same Network

Part 4: Check the Network

Part 5: Clean Up

The Problem With What We Just Did

πŸ“‹ Network Comparison Quick one-liner: Connect your containers to each other and the outside world. Learn bridge networks, port mapping, DNS, and container-to-container communication. In the last post, you got Redis + RedisInsight working. Your data persisted across a version upgrade, your config was pre-loaded via bind mount, and your Redis data survived the container being deleted and recreated. But remember what happened when we tried to connect RedisInsight to Redis? Hostname didn't resolve. We had to fall back to docker inspect to find the Redis IP (172.17.0.2) and connect that way. That works for a quick test, but it's not how you want to run anything real: Today, we're fixing all of that. We'll cover: By the end, you'll have RedisInsight talking to Redis by name, accessible from your browser, on a clean isolated network. The right way. When you run a container without specifying a network, Docker puts it on the default bridge network: Containers on the default bridge can reach each other by IP address, but not by name. That's exactly what you saw with RedisInsight and Redis in the last post. Let's reproduce it with the same setup β€” the Redis + RedisInsight you ended Ep 5 with: No -p flag β€” so you can't reach it from your browser, just like in Ep 5. Try resolving dtredis86 from the RedisInsight container: Hostname resolution doesn't work on the default bridge. You'd need to inspect the container to find its IP, then connect that way β€” exactly what you saw in Ep 5 when we had to fall back to 172.17.0.2. This is fragile. If you restart the containers, the IPs change. Let's fix this properly. In Ep 5, you ran RedisInsight without port mapping and couldn't reach it at http://localhost:5540. The container was alive inside, but your host had no way in. Let's add port mapping to the same RedisInsight setup from Ep 5: The ./data/ri directory should already have the correct chown from Ep 5. If not: Now open http://localhost:8080 β€” RedisInsight loads. πŸŽ‰ The format is always host_port:container_port: Now here's a practical exercise: open RedisInsight at http://localhost:8080 and try adding dtredis2 as a new connection: The only way to reach dtredis2 from RedisInsight is via your host's IP address. Get it with: Then in RedisInsight: redis://<your_host_ip>:6380 β€” it connects, but it's ugly. If your host IP changes, you're updating configs everywhere. It works β€” but we're using a raw IP address that could change at any time. Verify the port mapping: There has to be a better way. πŸ‘‡ Rootless note: In rootless mode, you can't bind to ports below 1024. So -p 80:5540 won't work. Use -p 8080:5540 or higher. If you need port 80, put a rootful reverse proxy (nginx, caddy, traefik) in front. Clean up before moving on: Create a custom bridge network. Containers on the same custom network can reach each other by name β€” Docker provides built-in DNS resolution. Run Redis and RedisInsight on this network: First, verify the connection from the command line: It resolves. βœ… No IP inspection, no manual config. The name works automatically. Now open http://localhost:8080 β€” RedisInsight loads, and you can add dtredis:6379 as a new connection. No IP addresses needed. Connect an existing container to a network: This container is on the default bridge. Connect it to dtapp: Now dtredis2 is on both networks. Verify ri can reach it: Redis responds by hostname. βœ… Let's wire up a real multi-container stack β€” PostgreSQL for the database, CloudBeaver as the web UI. Rootless note: PostgreSQL tries to chmod /var/run/postgresql for its PID file at startup. Rootless containers can't do that. The --tmpfs flag gives it a writable temp directory on that path without root. Wait ~30 seconds for it to start, then open http://localhost:8978 in your browser. It loads. πŸŽ‰ Now add a connection in the CloudBeaver UI: You'll see both containers listed under Containers: Two containers. One network. DNS works between them. Port mapping gives you browser access. This is how multi-container apps should run. Look at what it took to wire up two containers: Three commands. That's not the problem. There's a better way. πŸ‘‰ Coming up: We'll replace every single command above with a clean YAML file. Docker Compose β€” define your entire multi-container stack and launch it with one command. πŸ“š Want More? This guide covers the basics from Chapter 5: Docker Networking in my book, "Levelling Up with Docker" β€” 14 chapters of practical, hands-on Docker guides. πŸ“š Grab the book: "Levelling Up with Docker" on Amazon Found this helpful? πŸ™Œ Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# echo PING | nc dtredis86 6379 nc: bad address 'dtredis86' # echo PING | nc dtredis86 6379 nc: bad address 'dtredis86' # echo PING | nc dtredis86 6379 nc: bad address 'dtredis86' $ -weight: 500;">docker network ls NETWORK ID NAME DRIVER SCOPE a1b2c3d4e5f6 bridge bridge local d4e5f6a1b2c3 host host local e5f6a1b2c3d4 none null local $ -weight: 500;">docker network ls NETWORK ID NAME DRIVER SCOPE a1b2c3d4e5f6 bridge bridge local d4e5f6a1b2c3 host host local e5f6a1b2c3d4 none null local $ -weight: 500;">docker network ls NETWORK ID NAME DRIVER SCOPE a1b2c3d4e5f6 bridge bridge local d4e5f6a1b2c3 host host local e5f6a1b2c3d4 none null local $ -weight: 500;">docker run -d --rm --name dtredis86 \ -v redisdata:/data \ -v ./redis.conf:/etc/redis/redis.conf \ redis:8.6 redis-server /etc/redis/redis.conf $ -weight: 500;">docker run -d --rm --name redisinsight \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker run -d --rm --name dtredis86 \ -v redisdata:/data \ -v ./redis.conf:/etc/redis/redis.conf \ redis:8.6 redis-server /etc/redis/redis.conf $ -weight: 500;">docker run -d --rm --name redisinsight \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker run -d --rm --name dtredis86 \ -v redisdata:/data \ -v ./redis.conf:/etc/redis/redis.conf \ redis:8.6 redis-server /etc/redis/redis.conf $ -weight: 500;">docker run -d --rm --name redisinsight \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker exec redisinsight sh -c "echo PING | nc dtredis86 6379" nc: bad address 'dtredis86' $ -weight: 500;">docker exec redisinsight sh -c "echo PING | nc dtredis86 6379" nc: bad address 'dtredis86' $ -weight: 500;">docker exec redisinsight sh -c "echo PING | nc dtredis86 6379" nc: bad address 'dtredis86' $ -weight: 500;">docker -weight: 500;">stop redisinsight $ -weight: 500;">docker run -d --rm --name redisinsight \ -p 8080:5540 \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker -weight: 500;">stop redisinsight $ -weight: 500;">docker run -d --rm --name redisinsight \ -p 8080:5540 \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker -weight: 500;">stop redisinsight $ -weight: 500;">docker run -d --rm --name redisinsight \ -p 8080:5540 \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 600;">sudo chown -R 1001000:1001000 ./data/ri $ -weight: 600;">sudo chown -R 1001000:1001000 ./data/ri $ -weight: 600;">sudo chown -R 1001000:1001000 ./data/ri $ -weight: 500;">docker run -d --rm --name dtredis2 -p 6380:6379 redis $ -weight: 500;">docker run -d --rm --name dtredis2 -p 6380:6379 redis $ -weight: 500;">docker run -d --rm --name dtredis2 -p 6380:6379 redis $ hostname -I <your_host_ip> $ hostname -I <your_host_ip> $ hostname -I <your_host_ip> $ -weight: 500;">docker port dtredis2 6379/tcp -> 0.0.0.0:6380 $ -weight: 500;">docker port dtredis2 6379/tcp -> 0.0.0.0:6380 $ -weight: 500;">docker -weight: 500;">stop dtredis86 redisinsight $ -weight: 500;">docker -weight: 500;">stop dtredis86 redisinsight $ -weight: 500;">docker -weight: 500;">stop dtredis86 redisinsight $ -weight: 500;">docker network create dtapp $ -weight: 500;">docker network create dtapp $ -weight: 500;">docker network create dtapp $ -weight: 500;">docker run -d --rm --name dtredis --network dtapp redis $ -weight: 500;">docker run -d --rm --name ri --network dtapp \ -p 8080:5540 \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker run -d --rm --name dtredis --network dtapp redis $ -weight: 500;">docker run -d --rm --name ri --network dtapp \ -p 8080:5540 \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker run -d --rm --name dtredis --network dtapp redis $ -weight: 500;">docker run -d --rm --name ri --network dtapp \ -p 8080:5540 \ -v ./data/ri:/data \ -e RI_PRE_SETUP_DATABASES_PATH=/data/config.json \ -e RI_ACCEPT_TERMS_AND_CONDITIONS=true \ redis/redisinsight $ -weight: 500;">docker exec ri sh -c "echo PING | nc dtredis 6379" $ -weight: 500;">docker exec ri sh -c "echo PING | nc dtredis 6379" $ -weight: 500;">docker exec ri sh -c "echo PING | nc dtredis 6379" $ -weight: 500;">docker run -d --rm --name dtredis2 redis $ -weight: 500;">docker run -d --rm --name dtredis2 redis $ -weight: 500;">docker run -d --rm --name dtredis2 redis $ -weight: 500;">docker network connect dtapp dtredis2 $ -weight: 500;">docker network connect dtapp dtredis2 $ -weight: 500;">docker network connect dtapp dtredis2 $ -weight: 500;">docker exec ri sh -c "echo PING | nc dtredis2 6379" PING +PONG $ -weight: 500;">docker exec ri sh -c "echo PING | nc dtredis2 6379" PING +PONG $ -weight: 500;">docker exec ri sh -c "echo PING | nc dtredis2 6379" PING +PONG $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg \ --network dtstack \ -e POSTGRES_PASSWORD=-weight: 500;">docker \ -e POSTGRES_DB=testdb \ -v pgdata:/var/lib/postgresql/data \ --tmpfs /var/run/postgresql \ postgres:17 $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg \ --network dtstack \ -e POSTGRES_PASSWORD=-weight: 500;">docker \ -e POSTGRES_DB=testdb \ -v pgdata:/var/lib/postgresql/data \ --tmpfs /var/run/postgresql \ postgres:17 $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg \ --network dtstack \ -e POSTGRES_PASSWORD=-weight: 500;">docker \ -e POSTGRES_DB=testdb \ -v pgdata:/var/lib/postgresql/data \ --tmpfs /var/run/postgresql \ postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver \ --network dtstack \ -p 8978:8978 \ -v cbdata:/opt/cloudbeaver/workspace \ dbeaver/cloudbeaver:latest $ -weight: 500;">docker run -d --rm --name cloudbeaver \ --network dtstack \ -p 8978:8978 \ -v cbdata:/opt/cloudbeaver/workspace \ dbeaver/cloudbeaver:latest $ -weight: 500;">docker run -d --rm --name cloudbeaver \ --network dtstack \ -p 8978:8978 \ -v cbdata:/opt/cloudbeaver/workspace \ dbeaver/cloudbeaver:latest $ -weight: 500;">docker network inspect dtstack $ -weight: 500;">docker network inspect dtstack $ -weight: 500;">docker network inspect dtstack "Containers": { "abc123...": { "Name": "dtpg", "IPv4Address": "172.18.0.2/16" }, "def456...": { "Name": "cloudbeaver", "IPv4Address": "172.18.0.3/16" } } "Containers": { "abc123...": { "Name": "dtpg", "IPv4Address": "172.18.0.2/16" }, "def456...": { "Name": "cloudbeaver", "IPv4Address": "172.18.0.3/16" } } "Containers": { "abc123...": { "Name": "dtpg", "IPv4Address": "172.18.0.2/16" }, "def456...": { "Name": "cloudbeaver", "IPv4Address": "172.18.0.3/16" } } $ -weight: 500;">docker -weight: 500;">stop dtpg cloudbeaver $ -weight: 500;">docker volume rm pgdata cbdata $ -weight: 500;">docker network rm dtstack $ -weight: 500;">docker -weight: 500;">stop dtpg cloudbeaver $ -weight: 500;">docker volume rm pgdata cbdata $ -weight: 500;">docker network rm dtstack $ -weight: 500;">docker -weight: 500;">stop dtpg cloudbeaver $ -weight: 500;">docker volume rm pgdata cbdata $ -weight: 500;">docker network rm dtstack $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg --network dtstack -e POSTGRES_PASSWORD=-weight: 500;">docker -e POSTGRES_DB=testdb -v pgdata:/var/lib/postgresql/data --tmpfs /var/run/postgresql postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver --network dtstack -p 8978:8978 -v cbdata:/opt/cloudbeaver/workspace dbeaver/cloudbeaver:latest $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg --network dtstack -e POSTGRES_PASSWORD=-weight: 500;">docker -e POSTGRES_DB=testdb -v pgdata:/var/lib/postgresql/data --tmpfs /var/run/postgresql postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver --network dtstack -p 8978:8978 -v cbdata:/opt/cloudbeaver/workspace dbeaver/cloudbeaver:latest $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg --network dtstack -e POSTGRES_PASSWORD=-weight: 500;">docker -e POSTGRES_DB=testdb -v pgdata:/var/lib/postgresql/data --tmpfs /var/run/postgresql postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver --network dtstack -p 8978:8978 -v cbdata:/opt/cloudbeaver/workspace dbeaver/cloudbeaver:latest - πŸ—οΈ If you -weight: 500;">restart the container, the IP changes - πŸ“‹ You'd need to manually -weight: 500;">update configs every time - πŸ”’ No network isolation β€” anything can reach anything - 🌐 No way to access containers from your browser - Port mapping β€” exposing container ports to your browser - Custom bridge networks β€” containers that find each other by name - DNS resolution β€” no more -weight: 500;">docker inspect to find IPs - Network isolation β€” keeping your services segmented - Docker installed (rootless mode recommended) - Ep 1-5 completed β€” you know volumes, environment variables, and bind mounts - 5 minutes to wire up your first multi-container setup - Server: dtpg - Username: postgres - Password: -weight: 500;">docker - Database: testdb - The second command is a 150-character wall of flags, environment variables, and volume mounts - One typo in --tmpfs and PostgreSQL silently starts but fails to accept connections - Forget --network dtstack and the containers won't find each other - Tear it down and rebuild? Type it all again - What about when you have 5 containers? 10? Shared volumes? Secret files? Restart policies? - LinkedIn: Share with your network - Twitter: Tweet about it - Questions? Drop a comment below or reach out on LinkedIn