Expand Authors Top

If you have a few years of experience in the Java ecosystem and you’d like to share that with the community, have a look at our Contribution Guidelines.

November Discount Launch 2022 – Top
We’re finally running a Black Friday launch. All Courses are 30% off until next Friday:

>> GET ACCESS NOW

November Discount Launch 2022 – TEMP TOP (NPI)
We’re finally running a Black Friday launch. All Courses are 30% off until next Friday:

>> GET ACCESS NOW

Expanded Audience – Frontegg – Security (partner)
announcement - icon User management is very complex, when implemented properly. No surprise here.

Not having to roll all of that out manually, but instead integrating a mature, fully-fledged solution - yeah, that makes a lot of sense.
That's basically what Frontegg is - User Management for your application. It's focused on making your app scalable, secure and enjoyable for your users.
From signup to authentication, it supports simple scenarios all the way to complex and custom application logic.

Have a look:

>> Elegant User Management, Tailor-made for B2B SaaS

1. Overview

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 focus on two YAML properties that allow us to customize networking between containers, expose and ports. We'll describe them in detail, explore the basic use cases, and highlight their key differences.

2. expose Section

First, 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 can see, we can specify multiple values for each service. We 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.

Now let's 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 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 with expose, 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.

Again, 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:

[HOST:]CONTAINER[/PROTOCOL]

Here, HOST is a host port number or 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 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'll 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 can be moved to the short one due to the lack of a mode property.

4. Conclusion

In this article, we covered part of the 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 section, 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. Finally, depending on our preferences, we can choose between the two different syntaxes.

November Discount Launch 2022 – Bottom
We’re finally running a Black Friday launch. All Courses are 30% off until next Friday:

>> GET ACCESS NOW

Generic footer banner
Comments are closed on this article!