Networking Basics with Docker Compose
We'll cover published ports, IP addresses, gateways and how Docker Compose makes DNS entries for each service.
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 asa:5678
- The
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 theexample_default
network’sId
IPAddress
matches what we saw in the network’s outputAliases
includes our original DNS name and a newhttp-client
name- Docker Compose added this for us automatically!
- We can add our own custom aliases too, more on that soon
DNSNames
combinesAliases
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 to0.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
- If you tried using
- 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!
- In this case you may want to use
- It’s intended for containers to talk back with the host
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.