Docker and Kubernetes for Java Developers
上QQ阅读APP看书,第一时间看更新

Exposing ports and mapping ports

A common scenario is usually when you want your containerized application to accept incoming connections, either from other containers or from outside of Docker. It can be an application server listening on port 80 or a database accepting incoming requests.

An image can expose ports. Exposing ports means that your containerized application will listen on an exposed port. As an example, the Tomcat application server will listen on the port 8080 by default. All containers running on the same host and on the same network can communicate with Tomcat on this port. Exposing a port can be done in two ways. It can be either in the Dockerfile with the EXPOSE instruction (we will do this in the chapter about creating images later) or in the docker run command using the --expose option. Take this official Tomcat image Dockerfile fragment (note that it has been shortened for clarity of the example):

FROM openjdk:8-jre-alpine
    
ENV CATALINA_HOME /usr/local/tomcat
    
ENV PATH $CATALINA_HOME/bin:$PATH
    
RUN mkdir -p "$CATALINA_HOME"
    
WORKDIR $CATALINA_HOME
    
EXPOSE 8080
    
CMD ["catalina.sh", "run"]  

As you can see, there's an EXPOSE 8080 instruction near the end of the Dockerfile. It means that we could expect that the container, when run, will listen on port number 8080. Let's run the latest Tomcat image again. This time, we will also give our container a name, myTomcat. Start the application server using the following command:

docker run -it --name myTomcat --net=myNetwork tomcat  

For the purpose of checking if containers on the same network can communicate, we will use another image, busybox. BusyBox is software that provides several stripped-down Unix tools in a single executable file. Let's run the following command in the separate shell or command prompt window:

docker run -it --net container:myTomcat busybox  

As you can see, we have instructed Docker that we want our busybox container to use the same network as Tomcat uses. As an alternative, we could of course go with specifying a network name explicitly, using the --net myNetwork option.

Let's check if they indeed can communicate. Execute the following in the shell window with busybox running:

$ wget localhost:8080

The previous instruction will execute the HTTP GET request on port 8080, on which Tomcat is listening in another container. After the successful download of Tomcat's index.html, we have proof that both containers can communicate:

So far so good, containers running on the same host and the same network can communicate with each other. But what about communicating with our container from the outside? Mapping ports comes in handy. We can map a port, exposed by the Docker container, into the port of the host machine, which will be a localhost in our case. The general idea is that we want the port on the host to be mapped to a specific port in the running container, the same as port number 8080 of the Tomcat container.

To bind a port (or group of ports) from a host to the container, we use the -p flag of the docker run command, as in the following example:

$ docker run -it --name myTomcat2 --net=myNetwork -p 8080:8080 tomcat  

The previous command runs another Tomcat instance, also connected to the myNetwork network. This time, however, we map the container's port 8080 to the host's port of the same number. The syntax of the -p switch is quite straightforward: you just enter the host port number, a colon, and then a port number in the container you would like to be mapped:

$ docker run -p <hostPort>:<containerPort> <image ID or name> 

The Docker image can expose a whole range of ports to other containers using either the EXPOSE instruction in a Dockerfile (the same as EXPOSE 7000-8000, for example) or the docker run command, for example:

$ docker run --expose=7000-8000 <container ID or name>  

You can then map a whole range of ports from the host to the container by using the docker run command:

$ docker run -p 7000-8000:7000-8000 <container ID or name>  

Let's verify if we can access the Tomcat container from outside of Docker. To do this, let's run Tomcat with mapped ports:

$ docker run -it --name myTomcat2 --net=myNetwork -p 8080:8080 tomcat   

Then, we can simply enter the following address in our favorite web browser: http://localhost:8080.

As a result, we can see Tomcat's default welcome page, served straight from the Docker container running, as you can see in the following screenshot:

Good, we can communicate with our container from the outside of Docker. By the way, we now have two isolated Tomcats running on the host, without any port conflicts, resource conflicts, and so on. This is the power of containerization.

You may ask, what is the difference between exposing and mapping ports, that is, between --expose switch and -p switches? Well, the --expose will expose a port at runtime but will not create any mapping to the host. Exposed ports will be available only to another container running on the same network, on the same Docker host. The -p option, on the other hand, is the same as publish: it will create a port mapping rule, mapping a port on the container with the port on the host system. The mapped port will be available from outside Docker. Note that if you do -p, but there is no EXPOSE in the Dockerfile, Docker will do an implicit EXPOSE. This is because, if a port is open to the public, it is automatically also open to other Docker containers.

There is no way to create a port mapping in the Dockerfile. Mapping a port or ports is, just a runtime option. The reason for that is because port mapping configuration depends on the host. The Dockerfile needs to be host-independent and portable.

You can bind a port using -p in the runtime only.

There is yet one more option, which allows you to map all ports exposed in an image (that is; in the Dockerfile) at once, automatically during the container startup. The -P switch (capital P this time) will map a dynamically allocated random host port to all container ports that have been exposed in the Dockerfile by the EXPOSE instruction.

The -p option gives you more control than -P when mapping ports. Docker will not automatically pick any random port; it's up to you what ports on the host should be mapped to the container ports.

If you run the following command, Docker will map a random port on the host to Tomcat's exposed port number 8080:

$ docker run -it --name myTomcat3 --net=myNetwork -P tomcat

To check exactly which host port has been mapped, you can use the docker ps command. This is probably the quickest way of determining the current port mapping. The docker ps command is used to see the list of running containers. Execute the following from a separate shell console:

$ docker ps

In the output, Docker will list all running containers, showing which ports have been mapped in the PORTS column:

As you can see in the previous screenshot, our myTomcat3 container will have the 8080 port mapped to port number 32772 on the host. Again, executing the HTTP GET method on the http://localhost:32772 address will give us myTomcat3's welcome page. An alternative to the docker ps command is the docker port command, used with the container ID or with a name as a parameter (this will give you information about what ports have been mapped). In our case, this will be:

$ docker port myTomcat3

As a result, Docker will output the mapping, saying that port number 80 from the container has been mapped to port number 8080 on the host machine:

Information about all the port mappings is also available in the result of the docker inspect command. Execute the following command, for example:

$ docker inspect myTomcat2

In the output of the docker inspect command, you will find the Ports section containing the information about mappings:

Let's briefly summarize the options related to exposing and mapping ports in a table:

Instruction

Meaning

EXPOSE

Signals that there is service available on the specified port. Used in the Dockerfile and makes exposed ports open for other containers.

--expose

The same as EXPOSE but used in the runtime, during the container startup.

-p hostPort:containerPort

Specify a port mapping rule, mapping the port on the container with the port on the host machine. Makes a port open from the outside of Docker.

-P

Map dynamically allocated random port (or ports) of the host to all ports exposed using EXPOSE or --expose.

 

Mapping ports is a wonderful feature. It gives you flexible configuration possibilities to open your containers to the external world. In fact, it's indispensable if you want your containerized web server, database, or messaging server to be able to talk to others. If a default set of network drivers is not enough, you can always try to find a specific driver on the Internet or develop one yourself. Docker Engine network plugins extend Docker to support a wide range of networking technologies, such as IPVLAN, MACVLAN, or something completely different and exotic. Networking possibilities are almost endless in Docker. Let's focus now on another very important aspect of Docker container extensibility volumes.