As we know, Docker Compose is a tool for defining and managing multiple containers at once. By default, Docker Compose sets up a dedicated network for the defined containers, enabling communication between them. As a result, we can create and run services with a given configuration file using a single command.
In this tutorial, we'll see two YAML properties that allow us to customize networking between containers – expose and ports. We'll describe them in detail, show the basic use cases, and highlight their key differences.
2. expose Section
Firstly, let's look at the expose configuration. This property defines the ports that Docker Compose exposes from the container.
These ports will be accessible by other services connected to the same network but won't be published on the host machine.
We can expose a port by specifying its number in the services section:
services: myapp1: ... expose: - "3000" - "8000" myapp2: ... expose: - "5000"
As we see, we can specify multiple values for each service. We've just exposed the ports 3000 and 8000 from the myapp1 container and the port 5000 from the myapp2 container. The services are now accessible on these ports for other containers in the same network.
Let's now check exposed ports:
> docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8673c14f18d1 ... ... ... ... 3000/tcp, 8000/tcp bael_myapp1 bc044e180131 ... ... ... ... 5000/tcp bael_myapp2
In the docker ps command output, we can find the exposed ports in the PORTS column.
Finally, let's verify the communication between the containers:
> docker exec -it bc044e180131 /bin/bash bash-5.1$ nc -vz myapp1 3000 myapp1 (172.18.0.1:3000) open bash-5.1$ nc -vz myapp1 8000 myapp1 (172.18.0.1:8000) open
We've just connected to the myapp2 CLI. Using the netcat command, we checked that both the ports exposed from myapp1 were reachable.
3. ports Section
Now let's check the ports section. As before, this property defines the ports that we want to expose from the container. But unlike with the expose configuration, these ports will be accessible internally and published on the host machine.
Same as before, we can define ports for each service in the dedicated section, but the configuration might be more complex. First, we have to choose between two syntaxes (short and long) to define the configuration.
3.1. Short Syntax
Let's start by analyzing the short one. The short syntax is a colon-separated string to set the host IP address, host port, and container port:
Here, HOST is a host port number or a range of port numbers that can be preceded by an IP address. If we don't specify the IP address, Docker Compose binds the port to all the network interfaces.
CONTAINER defines a container port number or a range of port numbers.
PROTOCOL restricts container ports to the specified protocol or sets them to TCP if empty. Only the CONTAINER part is mandatory.
Now that we know the syntax, let's define the ports in our Docker Compose file:
services: myapp1: ... ports: - "3000" # container port (3000), assigned to random host port - "3001-3005" # container port range (3001-3005), assigned to random host ports - "8000:8000" # container port (8000), assigned to given host port (8000) - "9090-9091:8080-8081" # container port range (8080-8081), assigned to given host port range (9090-9091) - "127.0.0.1:8002:8002" # container port (8002), assigned to given host port (8002) and bind to 127.0.0.1 - "6060:6060/udp" # container port (6060) restricted to UDP protocol, assigned to given host (6060)
As presented above, we can also publish multiple container ports at once, using different variants of the short syntax and configuring it more precisely. Docker Compose exposes all specified container ports, making them reachable internally and externally from the local machine.
As before, let's check the exposed ports with the docker ps command:
> docker ps -a CONTAINER ID ... PORTS NAMES e8c65b9eec91 ... 0.0.0.0:51060->3000/tcp, 0.0.0.0:51063->3001/tcp, 0.0.0.0:51064->3002/tcp, bael_myapp1 0.0.0.0:51065->3003/tcp, 0.0.0.0:51061->3004/tcp, 0.0.0.0:51062->3005/tcp, 0.0.0.0:8000->8000/tcp, 0.0.0.0:9090->8080/tcp, 0.0.0.0:9091->8081/tcp 127.0.0.1:8002->8002/tcp, 0.0.0.0:6060->6060/udp
Once again, in the PORTS column, we can find all the exposed ports. The value to the left of the arrow shows the host address where we can reach the container externally.
3.2. Long Syntax
Using the long syntax, we can configure the ports in the same way. However, instead of using a colon-separated string, we define each property individually:
services: myapp1: ... ports: # - "127.0.0.1:6060:6060/udp" - target: 6060 host_ip: 127.0.0.1 published: 6060 protocol: udp mode: host
Here, the target is mandatory and specifies the container port (or range of ports) that will be exposed, which is equivalent to the CONTAINER in the short syntax.
The host_ip and published are parts of HOST in the short one, where we can define the host's IP address and port.
The protocol, the same as PROTOCOL in the short syntax, restricts the container port to the specified protocol (or TCP if empty).
The mode is the enum with two values that specifies port publishing rules. We should use the host value to publish a port locally. The second value – ingress – is reserved for more complex container environments and means the port will be load balanced.
In conclusion, any short syntax string can easily be represented by a long structure. However, not all long syntax configurations might be moved to the short one due to the lack of a mode property.
In this article, we've just covered a part of networking configurations in the Docker Compose. We analyzed and compared port configuration using the expose and ports sections.
The expose section allows us to expose specific ports from our container only to other services on the same network. We can do this simply by specifying the container ports.
The ports section also exposes specified ports from containers. Unlike the previous one, ports are open not only for other services on the same network but also to the host. The configuration is a bit more complex, where we can configure the exposed port, local binding address, and restricted protocol. Depending on our preferences, we can choose between the two different syntaxes.