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