Exposing container services

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:

  1. Assign a private IP address to the container, which is not reachable from an external network.
  2. Assign an IP address to the container outside the host's IP network.

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.

Publishing container ports – the -p option

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.

Publishing container ports – the -p option

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:

Publishing container ports – the -p option

Network Address Translation for containers

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:

  • The tcp keyword signifies that this DNAT rule applies only to the TCP transport protocol.
  • The first 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.
  • The second 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.
  • Finally, 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.

Retrieving the container port

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:

  • The 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
    
  • The 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:

    • The 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": {}
              },
      
    • The 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"
                      }
                  ]
              },
      
    • The 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.

  • The 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.

Retrieving the container port

Binding a container to a specific IP address

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.

Auto-generating the Docker host port

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:

  • The following text is an excerpt from the output of the 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
    
  • 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      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.

Port binding using EXPOSE and the -P option

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:

  • The following text is an excerpt from the output of the 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
    
  • The following text is an excerpt from the output of the 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.

Port binding using EXPOSE and the -P option
Port binding using EXPOSE and the -P option
Port binding using EXPOSE and the -P option
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset