06 Breaking Through the Container Wall How to Interact With the Outside World

06 Breaking Through the Container Wall How to Interact with the Outside World #

Hello, I’m Chrono.

In the previous lessons, we have learned about the concepts and usage of containers, images, and image repositories. We also know how to create images and launch applications in the form of containers.

However, while it’s relatively easy to use containers for running simple applications like “busybox” or “hello world,” it becomes more challenging when it comes to running backend service applications such as Nginx, Redis, or MySQL. These applications run within the container’s “sandbox” and are completely isolated from the outside world, making it impossible for them to provide services to the outside. As a result, the container’s isolation environment becomes a negative feature.

Therefore, the “small shed” represented by containers should not be a completely closed iron room. Instead, we should open a few doors and windows to allow applications to exchange data with the outside world and communicate effectively within the confines of their environment. This way, we can achieve “limited isolation,” which is the true purpose of our runtime environment.

Today, I will use Docker as an example to introduce the methods we can employ to enable communication and interaction between containers and external systems.

How to Copy Data within Containers #

Let’s first take a look at the cp command provided by Docker, which allows you to copy files between the host machine and the container. It is the most basic way to exchange data.

To experiment with this command, you need to start a container using the docker run command. Let’s use Redis as an example:

docker run -d --rm redis

Image

Note that we used the -d and --rm options, which mean running in the background and automatically removing the container after it finishes. You can use the docker ps command to see that the Redis container is running, and its container ID is “062”.

The usage of docker cp is very simple and similar to the “cp” or “scp” command on Linux. You just need to specify the source path and the destination path. If the source path is on the host machine, the file will be copied into the container. If the source path is in the container, the file will be copied out of the container. Note that you need to use the container name or container ID to specify which container the path belongs to.

Assuming there is a file named “a.txt” in the current directory, and we want to copy it into the “/tmp” directory of the Redis container. If using the container ID, the command would be like this:

docker cp a.txt 062:/tmp

Next, we can use the docker exec command to enter the container and check if the file has been copied correctly:

docker exec -it 062 sh

Image

As you can see, there is indeed an “a.txt” file in the “/tmp” directory.

Now let’s try copying a file from the container. You just need to swap the two paths in the docker cp command:

docker cp 062:/tmp/a.txt ./b.txt

This will create a new file named “b.txt” in the current directory of the host machine, which is the file we obtained from the container.

How to Share Files on a Host #

The usage of docker cp mimics the copy command of the operating system. Occasionally, it is sufficient to handle file sharing a few times, but if there is frequent file exchange during container runtime, repeatedly copying files back and forth can be cumbersome and prone to errors.

You may think of a feature called “shared directory” in virtual machines. It allows you to create a directory on the host and then “mount” this directory into the virtual machine, enabling both to share the same directory. Any operation on the files in the directory can be immediately seen on the other side, eliminating the need for file copying and significantly improving efficiency.

Following this idea, containers also provide the functionality of sharing a directory on the host. The effect is almost the same as that of virtual machines, and it is very convenient to use. You just need to use the -v parameter when launching a container with the docker run command. The specific format is “host path:container path”.

Taking Redis as an example again, when launching a container, you can use the -v parameter to mount the host’s “/tmp” directory into the container’s “/tmp” directory, which means letting the container share the host’s “/tmp” directory:

docker run -d --rm -v /tmp:/tmp redis

Then, we can use docker exec to enter the container and check the “/tmp” directory inside the container. We should see that the files are identical to those on the host.

docker exec -it b5a sh    # b5a is the container ID

You can also perform operations in the “/tmp” directory inside the container, such as deleting files or creating new directories. When you observe the host again, you will find that the changes are synchronized in real-time, indicating that the container and the host have indeed shared the directory.

The functionality of mounting a host directory with the -v parameter is very useful for our daily development and testing tasks. We can install any application in an image without changing the local environment, and then directly run our local source code or scripts in a container, which is very convenient.

Here, I’ll give a simple example. Suppose I only have Python 2.7 on my local machine, but I want to develop with Python 3. If I install Python 2 and Python 3 at the same time, it can easily mess up the system. So, I can do the following:

  1. First, use docker pull to pull a Python 3 image. Because it packages a complete runtime environment and runs in isolation, it will not affect the existing Python 2.7 on the system.

  2. Write Python code in a local directory and use the -v parameter to let the container share this directory.

  3. Now, I can install various packages with Python 3 in the container and run scripts for development.

docker pull python:alpine
docker run -it –rm -v `pwd`:/tmp python:alpine sh

Clearly, this approach is more flexible than packaging files into an image or using docker cp, making it very suitable for development and testing work that involves frequent modifications.

How to Achieve Network Connectivity #

Currently, we can solve the problem of file exchange between containers and the outside world using docker cp and docker run -v. However, for servers like Nginx and Redis, network connectivity is more important.

The key to network connectivity is to “bridge” the network between the inside and outside of the container. However, dealing with network communication is undoubtedly one of the trickiest tasks in computer systems. There are many terms, protocols, and tools involved, and I cannot explain them all at once. So I can only give you a general overview to help you understand quickly.

Docker provides three network modes: null, host, and bridge.

null is the simplest mode, which means no network. However, other network plugins can still customize network connections, but I won’t go into detail about that here.

host means directly using the host machine network. It removes network isolation of containers (other isolation remains), and all containers share the host machine’s IP address and network card. This mode has no intermediate layer, resulting in higher communication efficiency, but it lacks isolation, and running too many containers can easily lead to port conflicts.

To use the host mode, you can use the --net=host parameter when running the container using docker run. Let’s start Nginx using this parameter:

docker run -d --rm --net=host nginx:alpine

To verify the effect, we can execute the ip addr command on both the host machine and the container to view network card information:

ip addr                    # View network cards on the host machine
docker exec xxx ip addr    # View network cards in the container

图片

图片

As you can see, the output of these two ip addr commands is exactly the same. For example, they both have a network card named “ens160” and an IP address of “192.168.10.208”. This proves that the Nginx container does share the network stack with the host machine.

The third mode, bridge, is a bridged mode. It is similar to switches and routers in the real world, except that it is virtualized by software. Containers and the host machine connect to this bridge (docker0) through virtual network cards, enabling them to exchange network packets normally. However, compared to the host mode, the bridge mode has a virtual network bridge and network card, so the communication efficiency is a bit lower.

图片

Like the host mode, you can also enable the bridge mode using --net=bridge when running a container. However, it is not necessary because the default network mode of Docker is bridge, so it is generally not explicitly specified.

Next, let’s start two containers, Nginx and Redis, as mentioned earlier. Since we don’t specify anything special, the bridge mode will be used by default:

docker run -d --rm nginx:alpine    # Default bridge mode
docker run -d --rm redis           # Default bridge mode

Then, let’s execute the ip addr command again, on both the host machine and the container (since the Redis container does not have the ip command, we can only execute it in the Nginx container):

图片

Compare the output with that of the host mode, and you will find that the network card settings in the container are completely different from those on the host machine. eth0 is a virtual network card, and its IP address is a class B private address, “172.17.0.2”.

We can also use docker inspect to directly view the IP address of a container:

docker inspect xxx | grep IPAddress

图片

This displays the IP addresses of the two containers as “172.17.0.2” and “172.17.0.3”, while the IP address of the host machine is “172.17.0.1”. Therefore, they are all in the “172.17.0.0/16” Docker default network segment and can use IP addresses to achieve network communication with each other.

How to Allocate Server Port Numbers #

By using either the host mode or bridge mode, our containers are assigned an IP address and establish network connections with the outside world. The next issue to address is the allocation of port numbers for network services.

As you may know, server applications need to have port numbers in order to provide services to the outside world. For example, HTTP uses port 80, HTTPS uses port 443, Redis uses port 6379, and MySQL uses port 3306. In [Lesson 4], we learned about using the EXPOSE instruction in Dockerfile to declare the container’s external port number.

The number of available port numbers on a single host is limited, and multiple services cannot conflict with each other. However, when packaging image applications, we often use default port numbers, which can easily cause a container to fail to start due to a port conflict.

The solution to this problem is to add an “intermediate layer” and let the container environment, such as Docker, manage and allocate the port numbers. This layer performs a “mapping” operation between the host port and the container port, so the container still uses its own port number internally, but the external view is a different port number. This effectively avoids conflicts.

Port number mapping needs to be done using bridge mode, and when starting a container with docker run, you use the -p parameter, which is similar to the -v parameter for volume sharing and is separated by : between the host port and the container port. For example, if you want to start two Nginx containers, each running on ports 80 and 8080:

docker run -d -p 80:80 --rm nginx:alpine
docker run -d -p 8080:80 --rm nginx:alpine

This maps the host ports 80 and 8080 to the ports 80 within the two containers, avoiding conflicts. You can use curl to verify:

image

Using the docker ps command, you can see the port mapping more clearly in the “PORTS” column:

image

Summary #

Alright, today we have learned several methods for communication between containers and external systems.

You may have noticed that these methods almost eliminate the differences between containerized applications and local applications due to isolation. And thanks to the unique packaging mechanism of images, container technology obviously allows for more convenient installation of various applications compared to apt/yum, without “contaminating” the existing system.

In today’s lesson, I mentioned examples such as Python and Nginx. You can also draw inferences from them and learn from them by loading local configuration files into the container in the appropriate location, mapping the port numbers, and running Redis, MySQL, and Node.js all in the container, making the container a powerful assistant in our work.

Let’s briefly summarize the key points of this lesson:

  1. The docker cp command can copy files between containers and the host, suitable for simple data exchange.
  2. The docker run -v command allows containers to share local directories with the host, eliminating the need for copying and improving work efficiency.
  3. The host network mode allows containers to share the network stack with the host, which is efficient but easy to cause port conflicts.
  4. The bridge network mode implements a virtual bridge, allowing containers and the host to communicate with each other within a private network segment.
  5. The docker run -p command can map the host’s port number to the internal port number of the container, resolving potential port conflicts.

Homework #

Finally, it’s time for homework. I have two questions for you to think about:

  1. Can you explain the difference between the docker cp command we learned today and the COPY instruction in the Dockerfile from Lesson 4?
  2. What are the pros and cons of using host mode and bridge mode, and in what scenarios are they most suitable?

Feel free to leave your comments and let’s have a discussion. I will reply to you as soon as possible. If you find this helpful, please share it with your friends.

Next class will be a practical exercise. See you in the next class.

Image