Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Networking Basics with Docker Compose

networking-basics-with-docker-compose.jpg

We'll cover published ports, IP addresses, gateways and how Docker Compose makes DNS entries for each service.

Quick Jump:

Have you ever wondered how you’re able to automatically access something like web:8000 or db:5432 inside of your containers after upping your project with Docker Compose? Let’s unwind a few things and eventually get to that point.

Over the years I’ve written about a few networking concepts with Docker like exposing vs publishing ports and accessing host.docker.internal. If you’re looking on how to publish ports so localhost:8000 is available on your Docker host or you want to access services running on your Docker host from within a container check out those posts.

In this post we’re going to focus on how networks get created and set up for you automatically with Compose.

# A Basic Example

I like learning by example so let’s see the end result before we go into the details.

# compose.yaml

# Normally I wouldn't set the project name here but this is nice to make it self
# contained for a demo. Normally I set the COMPOSE_PROJECT_NAME env var. This
# will ensure our network and DNS names are the same for the sake of this post.
name: "example"

services:
  a:
    image: "hashicorp/http-echo"
    command: ["-text=Hello from A!"]
    stop_grace_period: "1s"

  b:
    image: "hashicorp/http-echo"
    command: ["-listen=:5679", "-text=Hello from B!"]
    stop_grace_period: "1s"

  http-client:
    image: "curlimages/curl"
    command: ["sleep", "infinity"]
    stop_grace_period: "1s"

The grace period is set to 1s because we’ll be upping / downing this project a bit, it’ll save us time!

If you already have a project named “example” and get an error with the command below then you can use something else or omit that so it uses your directory name by default.

Upping the Project

$ docker compose up

Network example_default   Created  0.1s

Container example-b-1     Created  0.4s
Container example-curl-1  Created  0.4s
Container example-a-1     Created  0.4s

Attaching to a-1, b-1, curl-1

a-1     | 2025/05/30 11:44:47 [INFO] server is listening on :5678
b-1     | 2025/05/30 11:44:47 [INFO] server is listening on :5679

So far so good.

Making a Few curl Requests

From our http-client container we can make curl requests to both services. http-echo echos a string of your choosing and it defaults to listening on 5678.

$ docker compose exec http-client curl a:5678
Hello from A!

$ docker compose exec http-client curl b:5679
Hello from B!

In the Docker Compose terminal output you should see the logs too:

a-1 | 2025/05/30 11:53:32 a:5678 172.27.0.4:34928 "GET / HTTP/1.1" 200 14 "curl/8.13.0" 18.001µs
b-1 | 2025/05/30 11:53:37 b:5679 172.27.0.4:58498 "GET / HTTP/1.1" 200 14 "curl/8.13.0" 7.7µs

172.27.0.4 is the IP address of the http-client container. It might be different for you. We’ll see how this works in a few.

All of this works because all 3 of these services are on the same network and Docker Compose wired all of that up out of the box.

The example Network

When we upped the project, example_default was created for us. That was in the Docker Compose logs near the top.

$ docker network inspect example_default

I’ve omit some of the output to keep it short but I kept the bits that are important for the sake of this post, such as IAM.Config and Containers.

Your values might look different depending on how you installed Docker and how many networks you already have. The likely difference will be the 172.27.X.X IP addresses. 27 in your case might be a lower or higher number based on how many networks you have.

[
    {
        "Name": "example_default",
        "Id": "47cedf526c6e76a95f969ea3e041ae2fbe6acd61ac08718ac0acdb45e5c6b1e9",
        "Created": "2025-05-30T11:50:09.456720487Z",
        "Scope": "local",
        "Driver": "bridge",
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.27.0.0/16",
                    "Gateway": "172.27.0.1"
                }
            ]
        },
        "ConfigOnly": false,
        "Containers": {
            "24c028f7e49d62225b15542e435b684ddac87675bf80d027dab1caf0b95609f2": {
                "Name": "example-http-client-1",
                "EndpointID": "e609c693af0e96e4220e9c94189ee4eaaa6193a945da32da6e7d9fa3036dfe57",
                "MacAddress": "2a:33:c0:0a:07:20",
                "IPv4Address": "172.27.0.4/16",
                "IPv6Address": ""
            },
            "324f50a7a70aa67c1040863c3f891ebd131cebb413ad661422af382064193cbe": {
                "Name": "example-a-1",
                "EndpointID": "e296a5899b503a23072a6dca0cf29ad189a0d17b0c5bf8adc387a3b77cf4b58d",
                "MacAddress": "72:30:b5:48:a4:55",
                "IPv4Address": "172.27.0.2/16",
                "IPv6Address": ""
            },
            "508ba4cb56a3bc060b2f623b8439623a31238d3ccee4e47314116f95b77d6a3b": {
                "Name": "example-b-1",
                "EndpointID": "96b0ff1090172d8585c155e2f427d6fe873821c3a49a052f2d0bc5b43f5f1950",
                "MacAddress": "ba:42:3f:18:60:f0",
                "IPv4Address": "172.27.0.3/16",
                "IPv6Address": ""
            }
        }
    }
]

Here’s a couple of takeaways:

  • It is a bridge network
  • It’s on a specific subnet (172.27.0.0/16)
    • This provides network separation between your different networks / projects
  • Each of your containers have their own IP address on the above subnet and a name
    • The Name is a DNS based way of accessing that container
      • ... http-client curl example-a-1:5678 works the same as a:5678

Solving the DNS Mystery

We can investigate the /etc/hosts file of a container:

$ docker compose exec http-client cat /etc/hosts

127.0.0.1   localhost
::1         localhost ip6-localhost ip6-loopback
fe00::      ip6-localnet
ff00::      ip6-mcastprefix
ff02::1     ip6-allnodes
ff02::2     ip6-allrouters
172.27.0.4  24c028f7e49d

Notice the last line’s 172.27.0.4 lines up with the IP address above. The 24c0 value is also the container name that’s the object key in the Containers above.

Now we can inspect that container (by name or ID) and check out its networking properties:

$ docker container inspect example-http-client-1
"Networks": {
    "example_default": {
        "IPAMConfig": null,
        "Links": null,
        "Aliases": [
            "example-http-client-1",
            "http-client"
        ],
        "MacAddress": "2a:33:c0:0a:07:20",
        "NetworkID": "47cedf526c6e76a95f969ea3e041ae2fbe6acd61ac08718ac0acdb45e5c6b1e9",
        "EndpointID": "e609c693af0e96e4220e9c94189ee4eaaa6193a945da32da6e7d9fa3036dfe57",
        "Gateway": "172.27.0.1",
        "IPAddress": "172.27.0.4",
        "DNSNames": [
            "example-http-client-1",
            "http-client",
            "24c028f7e49d"
        ]
    }
}

Here’s a couple of takeaways:

  • NetworkID matches the example_default network’s Id
  • IPAddress matches what we saw in the network’s output
  • Aliases includes our original DNS name and a new http-client name
    • Docker Compose added this for us automatically!
    • We can add our own custom aliases too, more on that soon
  • DNSNames combines Aliases and the container ID
    • This is what ultimately allows us to reach this container by any name included

Feel free to experiment on your own by making a request to a. At this point you have all the info you need to find out what a’s container name is (you can inspect it):

$ docker compose exec http-client curl 172.27.0.2:5678
Hello from A!

$ docker compose exec http-client curl a:5678
Hello from A!

$ docker compose exec http-client curl example-a-1:5678
Hello from A!

$ docker compose exec http-client curl 324f50a7a70a:5678
Hello from A!

Custom Aliases / DNS Names

If you have a use case where you want to customize which aliases are available for a service you can do that. Aliases are scoped to a specific network.

We can modify our a service to this:

  a:
    image: "hashicorp/http-echo"
    command: ["-text=Hello from A!"]
    stop_grace_period: "1s"
    networks:
      default:
        aliases:
          - "apple"

If you have a custom network you can change “default” to your network’s name.

You can stop and up the Docker Compose project again for the changes to apply.

Then you can access everything we did before and also docker compose exec http-client curl apple:5678. If you inspect the container like we did before you’ll see apple was added to both Aliases and DNSNames:

    "Aliases": [
        "example-a-1",
        "a",
        "apple"
    ],
    "DNSNames": [
        "example-a-1",
        "a",
        "apple",
        "6727d4beb382"
    ]

Bonus Tip on the Gateway IP Address

When we inspected the network and container we saw "Gateway": "172.27.0.1". All of the containers on that network have the same gateway IP address.

We can ping it from any container:

$ docker compose exec http-client ping 172.27.0.1

PING 172.27.0.1 (172.27.0.1): 56 data bytes
64 bytes from 172.27.0.1: seq=0 ttl=42 time=0.057 ms
64 bytes from 172.27.0.1: seq=1 ttl=42 time=0.146 ms
64 bytes from 172.27.0.1: seq=2 ttl=42 time=0.153 ms

With the way we have things set up we can’t do this which is expected:

$ docker compose exec http-client curl 172.27.0.1:5678

curl: (7) Failed to connect to 172.27.0.1 port 5678 after 0 ms: Could not connect to server

But technically if you publish a port back to your host for a service we could use the gateway’s IP address to access that service, such as 172.27.0.1:5678 and 172.27.0.1:5679.

For example, you can modify both services to this and re-up the project:

  a:
    image: "hashicorp/http-echo"
    command: ["-text=Hello from A!"]
    stop_grace_period: "1s"
    ports: ["5678:5678"]

  b:
    image: "hashicorp/http-echo"
    command: ["-listen=:5679", "-text=Hello from B!"]
    stop_grace_period: "1s"
    ports: ["5679:5679"]

Then suddenly this works along with everything else we did before:

$ docker compose exec http-client curl 172.27.0.1:5678
Hello from A!

$ docker compose exec http-client curl 172.27.0.1:5679
Hello from B!

When we publish the port and make a request to the gateway IP address this is getting routed back to the Docker host which forwards the request to the container.

Here’s a couple of takeaways:

  • 5678:5678 works because the left side is bound to 0.0.0.0 by default
    • If you tried using 127.0.0.1:5678:5678 the gateway IP address wouldn’t work on that port
  • In normal use cases you wouldn’t use the gateway IP to talk between containers
    • It’s intended for containers to talk back with the host
      • In this case you may want to use host.docker.internal instead!

I wanted to include this last tip because over the years when setting up all sorts of different networking things I sometimes noticed I could connect containers through the gateway IP address but other times I could not. I only recently learned it was due to publishing a port on all interfaces which enables that.

Clean Up

You can run docker compose down to stop everything, remove those stopped containers and also delete the example network that was created automatically.

$ docker compose down

Container example-curl-1  Removed  0.4s
Container example-b-1     Removed  0.4s
Container example-a-1     Removed  0.4s

Network example_default   Removed  0.1s

The video below runs all of the commands we covered above.

# Demo Video

Timestamps

  • 0:56 – The example compose project
  • 2:34 – Curling each service by name
  • 3:26 – Inspecting the Docker network
  • 5:07 – Connecting by other DNS names and IP addresses
  • 6:44 – Inspecting a container’s network properties
  • 8:47 – Creating a custom alias / DNS name
  • 10:04 – The gateway IP address
  • 12:22 – Cleaning things up

What’s your favorite Docker Compose networking tip? Let me know below.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per year (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments