So far, we have successfully launched an HTTP service and accessed the service from the Docker host as well as another container within the same Docker host. Furthermore, as demonstrated in the Build images from containers section of Chapter 4, Handling Docker Containers, the container is able to successfully install the wget
package by making a connection to the publicly available apt repository over the Internet. Nonetheless, the outside world cannot access the service offered by a container by default. At the outset, this might seem like a limitation in the Docker technology. However, the fact of the matter is, the containers are isolated from the outside world by design.
Docker achieves network isolation for the containers by the IP address assignment criteria, as enumerated:
Consequently, the Docker container is not reachable, even from the systems that are connected to the same IP network as the Docker host. This assignment scheme also provides protection from an IP address conflict that might otherwise arise.
Now, you might wonder how to make the services run inside a container that is accessible to the outside world, in other words, exposing container services. Well, Docker bridges this connectivity gap in a classy manner by leveraging the Linux iptables
functionality under the hood.
At the frontend, Docker provides two different building blocks to bridge this connectivity gap for its users. One of the building blocks is to bind the container port using the -p
(publish a container's port to the host interface) option of the docker run
subcommand. Another alternative is to use the combination of the EXPOSE
Dockerfile instruction and the -P
(publish all exposed ports to the host interfaces) option of the docker run
subcommand.
Docker enables you to publish a service offered inside a container by binding the container's port to the host interface. The -p
option of the docker run
subcommand enables you to bind a container port to a user-specified or auto-generated port of the Docker host. Thus, any communication destined for the IP address and the port of the Docker host would be forwarded to the port of the container. The -p
option, actually, supports the following four formats of arguments:
<hostPort>:<containerPort>
<containerPort>
<ip>:<hostPort>:<containerPort>
<ip>::<containerPort>
Here, <ip>
is the IP address of the Docker host, <hostPort>
is the Docker host port number, and <containerPort>
is the port number of the container. Here, in this section, we present you with the -p <hostPort>:<containerPort>
format and introduce other formats in the succeeding sections.
In order to understand the port binding process better, let's reuse the apache2
HTTP server image that we crafted previously, and spin up a container using a -p
option of the docker run
subcommand. The port 80
is the published port of the HTTP service, and as the default behavior, our apache2
HTTP server is also available on port 80
. Here, in order to demonstrate this capability, we are going to bind port 80
of the container to port 80
of the Docker host, using the -p <hostPort>:<containerPort>
option of the docker run
subcommand, as shown in the following command:
$ sudo docker run -d -p 80:80 apache2 baddba8afa98725ec85ad953557cd0614b4d0254f45436f9cb440f3f9eeae134
Now that we have successfully launched the container, we can connect to our HTTP server using any web browser from any external system (provided it has network connectivity) to reach our Docker host. So far, we have not added any web pages to our apache2
HTTP server image.
Hence, when we connect from a web browser, we will get the following screen, which is nothing but the default page that comes along with the Ubuntu Apache2
package:
In the previous section, we saw how a -p 80:80
option did the magic, didn't it? Well, in reality, under the hood, the Docker engine achieves this seamless connectivity by automatically configuring the Network Address Translation (NAT) rule in the Linux iptables
configuration files.
To illustrate the automatic configuration of the NAT rule in Linux iptables
, let's query the Docker hosts iptables
for its NAT entries, as follows:
$ sudo iptables -t nat -L -n
The ensuing text is an excerpt from the iptables
NAT entry, which is automatically added by the Docker engine:
Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.14:80
From the preceding excerpt, it is quite evident that the Docker engine has effectively added a DNAT
rule. The following are the details of the DNAT
rule:
tcp
keyword signifies that this DNAT
rule applies only to the TCP transport protocol.0.0.0.0/0
address is a meta IP address of the source address. This address indicates that the connection can originate from any IP address.0.0.0.0/0
address is a meta IP address of the destination address on the Docker host. This address indicates that the connection could be made to any valid IP address in the Docker host.dpt:80 to:172.17.0.14:80
is the forwarding instruction used to forward any TCP activity on port 80
of the Docker host to the IP address 172.17.0.17
, the IP address of our container and port 80
.Therefore, any TCP packet that the Docker host receives on port 80
will be forwarded to port 80
of the container.
The Docker engine provides at least three different options to retrieve the containers port binding details. Here, let's first explore the options, and then, move on to dissect the retrieved information. The options are as follows:
docker ps
subcommand always displays the port binding details of a container, as shown here:$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES baddba8afa98 apache2:latest "/usr/sbin/apache2ct 26 seconds ago Up 25 seconds 0.0.0.0:80->80/tcp furious_carson
docker inspect
subcommand is another alternative; however, you have to skim through quite a lot of details. Run the following command:$ sudo docker inspect baddba8afa98
The docker inspect
subcommand displays the port binding related information in three JSON objects, as shown here:
ExposedPorts
object enumerates all ports that are exposed through the EXPOSE
instruction in Dockerfile
, as well as the container ports that are mapped using the -p
option in the docker run
subcommand. Since we didn't add the EXPOSE
instruction in our Dockerfile
, what we have is just the container port that was mapped using -p80:80
as an argument to the docker run
subcommand:"ExposedPorts": { "80/tcp": {} },
PortBindings
object is part of the HostConfig
object, and this object lists out all the port binding done through the -p
option in the docker run
subcommand. This object will never list the ports exposed through the EXPOSE
instruction in the Dockerfile
:"PortBindings": { "80/tcp": [ { "HostIp": "", "HostPort": "80" } ] },
Ports
object of the NetworkSettings
object has the same level of detail, as the preceding PortBindings
object. However, this object encompasses all ports that are exposed through the EXPOSE
instruction in Dockerfile
, as well as the container ports that are mapped using the -p
option in the docker run
subcommand:"NetworkSettings": { "Bridge": "docker0", "Gateway": "172.17.42.1", "IPAddress": "172.17.0.14", "IPPrefixLen": 16, "PortMapping": null, "Ports": { "80/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "80" } ] } },
Of course, the specific port field can be filtered using the --format
option of the docker inspect
subcommand.
docker port
subcommand enables you to retrieve the port binding on the Docker host by specifying the container's port number:$ sudo docker port baddba8afa98 80 0.0.0.0:80
Evidently, in all the preceding output excerpts, the information that stands out is the IP address 0.0.0.0
and the port number 80
. The IP address 0.0.0.0
is a meta address, which represents all the IP addresses configured on the Docker host. In effect, the containers port 80
is bound to all the valid IP addresses on the Docker host. Therefore, the HTTP service is accessible through any of the valid IP addresses configured on the Docker host.
Until now, with the method that we have learnt, the containers always get bound to all the IP addresses configured in the Docker host. However, you may want to offer different services on different IP addresses. In other words, a specific IP address and port would be configured to offer a particular service. We can achieve this in Docker using the -p <ip>:<hostPort>:<containerPort>
option of the docker run
subcommand, as shown in the following example:
$ sudo docker run -d -p 198.51.100.73:80:80 apache2 92f107537bebd48e8917ea4f4788bf3f57064c8c996fc23ea0fd8ea49b4f3335
Here, the IP address must be a valid IP address on the Docker host. If the specified IP address is not a valid IP address on the Docker host, the container launch will fail with an error message, as follows:
2014/11/09 10:22:10 Error response from daemon: Cannot start container 99db8d30b284c0a0826d68044c42c370875d2c3cad0b87001b858ba78e9de53b: Error starting userland proxy: listen tcp 198.51.100.73:80: bind: cannot assign requested address
Now, let's quickly review the port mapping as well as the NAT entry for the preceding example.
The following text is an excerpt from the output of the docker ps
subcommand that shows the details of this container:
92f107537beb apache2:latest "/usr/sbin/apache2ct About a minute ago Up About a minute 198.51.100.73:80->80/tcp boring_ptolemy
The following text is an excerpt from the output of the iptables -n nat -L -n
command that shows the DNAT
entry created for this container:
DNAT tcp -- 0.0.0.0/0 198.51.100.73 tcp dpt:80 to:172.17.0.15:80
After reviewing both the output of the docker run
subcommand and the DNAT
entry of iptables
, you will realize how elegantly the Docker engine has configured the service offered by the container on the IP address 198.51.100.73
and port 80
of the Docker host.
The Docker containers are innately lightweight and due to their lightweight nature, you can run multiple containers with the same, or different services on a single Docker host. Particularly, auto scaling of the same service across several containers based on demand, is the need of IT infrastructure today. Here, in this section, you will be informed about the challenge in spinning up multiple containers with the same service, and also Docker's way of addressing this challenge.
Earlier in this chapter, we launched a container using apache2 http server
by binding it to port 80
of the Docker host. Now, if we attempt to launch one more container with the same port 80
binding, the container would fail to start with an error message, as you can see in the following example:
$ sudo docker run -d -p 80:80 apache2 6f01f485ab3ce81d45dc6369316659aed17eb341e9ad0229f66060a8ba4a2d0e 2014/11/03 23:28:07 Error response from daemon: Cannot start container 6f01f485ab3ce81d45dc6369316659aed17eb341e9ad0229f66060a8ba4a2d0e: Bind for 0.0.0.0:80 failed: port is already allocated
Obviously, in the preceding example, the container failed to start because the previous container is already mapped to 0.0.0.0
(all the IP addresses of the Docker host) and port 80
. In the TCP/IP communication model, the combination of the IP address, port, and the Transport Protocols (TCP, UDP, and so on) has to be unique.
We could have overcome this issue by manually choosing the Docker host port number (for instance, -p 81:80
or -p 8081:80
). Though this is an excellent solution, it does not perform well to auto-scaling scenarios. Instead, if we give the control to Docker, it would auto-generate the port number on the Docker host. This port number generation is achieved by underspecifying the Docker host port number, using the -p <containerPort>
option of the docker run
subcommand, as shown in the following example:
$ sudo docker run -d -p 80 apache2 ea3e0d1b18cff40ffcddd2bf077647dc94bceffad967b86c1a343bd33187d7a8
Having successfully started the new container with the auto-generated port, let's review the port mapping as well as the NAT entry for the preceding example:
docker ps
subcommand that shows the details of this container:ea3e0d1b18cf apache2:latest "/usr/sbin/apache2ct 5 minutes ago Up 5 minutes 0.0.0.0:49158->80/tcp nostalgic_morse
iptables -n nat -L -n
command that shows the DNAT
entry created for this container:DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49158 to:172.17.0.18:80
After reviewing both the output of the docker run
subcommand and the DNAT
entry of iptables
, what stands out is the port number 49158
. The port number 49158
is niftily auto-generated by the Docker engine on the Docker host, with the help of the underlying operating system. Besides, the meta IP address 0.0.0.0
implies that the service offered by the container is accessible from outside, through any of the valid IP addresses configured on the Docker host.
You may have a use case where you want to auto-generate the port number. However, if you still want to restrict the service to a particular IP address of the Docker host, you can use the -p <IP>::<containerPort>
option of the docker run
subcommand, as shown in the following example:
$ sudo docker run -d -p 198.51.100.73::80 apache2 6b5de258b3b82da0290f29946436d7ae307c8b72f22239956e453356532ec2a7
In the preceding two scenarios, the Docker engine auto-generated the port number on the Docker host and exposed it to the outside world. The general norm for network communication is to expose any service through a predefined port number so that anybody can know the IP address, and the port number can easily access the offered service. Whereas, here, the port numbers are auto-generated and as a result, the outside world cannot directly reach the offered service. So, the primary purpose of this method of container creation is to achieve auto-scaling, and the container created in this fashion would be interfaced with a proxy or load balance service on a predefined port.
So far, we have discussed the four distinct methods to publish a service running inside a container to the outside world. In all these four methods, the port binding decision is taken during the container launch time, and the image has no information about the ports on which the service is being offered. It has worked well so far because the image is being built by us, and we are pretty much aware of the port in which the service is being offered. However, in the case of third-party images, the port usage inside a container has to be published unambiguously. Besides, if we build images for third-party consumption or even for our own use, it is a good practice to explicitly state the ports in which the container offers its service. Perhaps, the image builders could ship a readme document along with the image. However, it is even better to embed the port details in the image itself so that you can easily find the port details from the image both manually as well as through automated scripts.
The Docker technology allows us to embed the port information using the EXPOSE
instruction in the Dockerfile
, which we introduced in Chapter 3, Building Images. Here, let's edit the Dockerfile
we used to build the apache2
HTTP server image earlier in this chapter, and add an EXPOSE
instruction, as shown in the following code. The default port for the HTTP service is port 80
, hence port 80
is exposed:
########################################### # Dockerfile to build an apache2 image ########################################### # Base image is Ubuntu FROM ubuntu:14.04 # Author: Dr. Peter MAINTAINER Dr. Peter <[email protected]> # Install apache2 package RUN apt-get update && apt-get install -y apache2 && apt-get clean # Set the log directory PATH ENV APACHE_LOG_DIR /var/log/apache2 # Expose port 80 EXPOSE 80 # Launch apache2 server in the foreground ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
Now that we have added the EXPOSE
instruction to our Dockerfile
, let's move to the next step of building the image using the docker build
command. Here, let's reuse the image name apache2
, as shown here:
$ sudo docker build -t apache2 .
Having successfully built the image, let's inspect the image to verify the effects of the EXPOSE
instruction to the image. As we learnt earlier, we can resort to the docker inspect
subcommand, as shown here:
$ sudo docker inspect apache2
On close review of the output generated by the preceding command, you will realize that Docker stores the exposed port information in the ExposedPorts
field of the Config
object. The following is an excerpt to show how the exposed port information is being displayed:
"ExposedPorts": { "80/tcp": {} },
Alternatively, you can apply the format option to the docker inspect
subcommand in order to narrow down the output to very specific information. In this case, the ExposedPorts
field of the Config
object is shown in the following example:
$ sudo docker inspect --format='{{.Config.ExposedPorts}}' apache2 map[80/tcp:map[]]
To resume our discussion on the EXPOSE
instruction, we can now spin up containers using an apache2
image, that we just crafted. Yet, the EXPOSE
instruction by itself cannot create port binding on the Docker host. In order to create port binding for the port declared using the EXPOSE
instruction, the Docker engine provides a -P
option in the docker run
subcommand.
In the following example, a container is launched from the apache2
image, which was rebuilt earlier. Here, the -d
option is used to launch the container in the detached mode, and the -P
option is used to create the port binding in the Docker host for all the ports declared, using the EXPOSE
instruction in the Dockerfile
:
$ sudo docker run -d -P apache2 fdb1c8d68226c384ab4f84882714fec206a73fd8c12ab57981fbd874e3fa9074
Now that we have started the new container with the image that was created using the EXPOSE
instruction, like the previous containers, let's review the port mapping as well as the NAT entry for the preceding example:
docker ps
subcommand that shows the details of this container:ea3e0d1b18cf apache2:latest "/usr/sbin/apache2ct 5 minutes ago Up 5 minutes 0.0.0.0:49159->80/tcp nostalgic_morse
iptables -t nat -L -n
command that shows the DNAT
entry created for this container:DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49159 to:172.17.0.19:80
The -P
option of the docker run
subcommand does not take any additional arguments, such as an IP address or a port number; consequently, fine tuning of the port binding is not possible, such as the -p
option of the docker run
subcommand. You can always resort to the -p
option of the docker run
subcommand if fine tuning of the port binding is critical to you.