DEV Community

Cover image for Understanding Process Isolation In Docker: An In-Depth Look at PID Namespaces
Kostas Kalafatis
Kostas Kalafatis

Posted on

Understanding Process Isolation In Docker: An In-Depth Look at PID Namespaces

Every running program - or process - has a unique number called a process identifier (PID). A PID namespace is the set of possible numbers that identify processes. Modern operating systems provide facilities to create multiple PID namespaces. Each namespace has a complete set of possible PIDs. This means that each PID namespace will contain its own 1, 2, 3, and so on.

From the perspective of a process in one namespace PID 1 might refer to an init system process like runit or supervisord. In a different namespace PID 1 might refer to a command shell like bash.

Creating a PID namespace for each container is a critical feature of Docker. We can run the following commands:

docker run -d --name namespaceA \
    busybox:latest /bin/sh -c "sleep 30000"

docker run -d --name namespaceB \
    busybox:latest /bin/sh -c "nc -l -p 80"
Enter fullscreen mode Exit fullscreen mode

The first command will create a container named namespaceA that runs a Busybox image and essentially does nothing for 8.3 hours before exiting automatically.

The second command will create a container named namespaceB that runs a Busybox image containing a simple web server using netcat. It will listen for incoming connections on port 80.

Now if we run the following command:

docker exec namespaceA ps
Enter fullscreen mode Exit fullscreen mode

we will get the following process list:

PID   USER     TIME  COMMAND
1     root      0:00 /bin/sh -c sleep 30000
12    root      0:00 ps
Enter fullscreen mode Exit fullscreen mode

and if we run the following command:

docker exec namespaceB ps
Enter fullscreen mode Exit fullscreen mode

we will get the following process list:

PID   USER     TIME  COMMAND
1     root      0:00 nc -l -p 80
7     root      0:00 ps
Enter fullscreen mode Exit fullscreen mode

In this example, we used the docker exec command to run additional processes in the running container. In this case, we are using the ps command, which shows all running processes and their PIDs. As we can see from the output, each container has its own PID 1 process.

This means that processes running inside a container would share the same ID space as processes running on the host or in other containers. Another thing a container could do is see what other processes were running on the host machine.

Even worse, namespaces change a lot of decisions about authorization into decisions about domains. That means that processes running in one container might be able to manage processes running in other containers. For lack of the PID namespace, Docker would not be nearly as useful.

Creating Containers without Separate Namespaces

Like most Docker isolation features, we can optionally create containers without their own PID namespace. We can do that by setting the --pid flag on docker create or docker run commands, and setting the value to host. You can try it yourself with the following command:

docker run --pid host busybox:latest ps
Enter fullscreen mode Exit fullscreen mode

This can lead to some issues with resource conflicts. Imagine that the following nginx instance is running directly on the host machine and not in Docker:

docker run -d --name host-nginx nginx:latest
Enter fullscreen mode Exit fullscreen mode

Then try to run another nginx process in the same container:

docker exec host-nginx nginx
Enter fullscreen mode Exit fullscreen mode

The last command will display the following:

2024/03/27 19:29:09 [emerg] 63#63: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2024/03/27 19:29:09 [emerg] 63#63: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2024/03/27 19:29:09 [notice] 63#63: try again to bind() after 500ms
2024/03/27 19:29:09 [emerg] 63#63: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2024/03/27 19:29:09 [emerg] 63#63: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2024/03/27 19:29:09 [notice] 63#63: try again to bind() after 500ms
2024/03/27 19:29:09 [emerg] 63#63: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2024/03/27 19:29:09 [emerg] 63#63: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2024/03/27 19:29:09 [notice] 63#63: try again to bind() after 500ms
2024/03/27 19:29:09 [emerg] 63#63: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2024/03/27 19:29:09 [emerg] 63#63: bind() to [::]:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
2024/03/27 19:29:09 [notice] 63#63: try again to bind() after 500ms
2024/03/27 19:29:09 [emerg] 63#63: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Enter fullscreen mode Exit fullscreen mode

The second process fails to start properly and reports that the address it needs is already in use. This is called a port conflict, and it is a common issue in real-world systems where several processes are running on the same machine, or multiple people contribute to the same environment.

How Programs Clash

To understand how programs might clash, let's imagine a fast-food restaurant. This restaurant has a few key elements: an ordering system, special menu items, and a limited number of tables.

  • Ordering Systems as Shared Resources: The way customers order food (counter service, mobile app, etc.) acts like a shared resource with a specific interface. If a customer is used to ordering in-person, they'll struggle in a restaurant that only takes mobile orders. Just like this, programs expecting certain libraries or system configurations can fail if those aren't in place.

  • Special Menu Items as Dependencies: Limited-time offers or regional specialties are akin to dependencies on specific software components. Imagine two popular menu items that both require a unique sauce. If the sauce runs out, neither item can be prepared correctly. Similarly, multiple programs needing an unusual or outdated library version may conflict if only one version can be installed.

  • Tables as Scarce Resources: Tables represent finite things like memory, CPU time, or open network ports. If the restaurant is packed and two groups want the same booth, there's a problem. Only one can use it at a time. Likewise, programs trying to use the same port or hogging system resources lead to conflicts.

  • Changing Signage as Environment Variables: Imagine if the signs directing customers to order pickup, restrooms, etc., were constantly changed while people were eating. Confusion would ensue! This mirrors how programs rely on environment variables or registry settings to find things they need. Messing with those variables breaks functionality.

Docker solves software conflicts with tools like Linux namespaces, file system roots, and virtualized network components. All these tools are used to provide isolation to each container.

Conclusion

In conclusion, Docker’s use of PID namespaces is a fundamental aspect of its ability to provide isolated environments for running applications. This isolation prevents conflicts between processes running in different containers, enhancing the reliability and stability of software deployments. However, it’s crucial to understand the potential issues that can arise when containers do not have separate PID namespaces. By understanding these concepts, developers and system administrators can better leverage Docker’s capabilities and avoid common pitfalls, leading to more robust and conflict-free application deployments. The analogy of a fast-food restaurant effectively illustrates how shared resources, dependencies, scarce resources, and environment variables can lead to conflicts, and how Docker addresses these issues. As we continue to push the boundaries of containerization, understanding these underlying principles will remain essential.

Top comments (0)