TT Lab
Get started
Learn Learning paths Courses

Container Internals

Seeing Namespaces With Your Own Eyes

Continue in TT Lab

This lab runs on a real VM

This box is not a Pod but a virtual machine started by KubeVirt. A Linux kernel of its own runs, systemd actually manages services, and docker is a real Docker engine, not an imitation. A container started with docker run becomes an actual process, and docker exec and docker logs work as usual.

This lab used to run inside a Pod. Since it was a box with all kernel capabilities dropped, the step of starting a container was blocked, so you learned through a workaround of unpacking the image archive yourself. The workaround is no longer needed.

There are two things to know.

Goal

You measure five of the seven namespaces yourself — pid, uts, mnt, net, and user — and prove with inode numbers that a container is not a 'box' but an ordinary process with a restricted view. At the end, you reproduce with Docker alone the structure of a Kubernetes Pod in which two containers share a network namespace.

Why it matters

Most of the points where container troubleshooting gets stuck come from not knowing "is this separated or not". Why ports conflict, where on the host a file seen inside a container is located, why a sidecar connects to the app over localhost — the answer to all of them is one line: "which namespace does that resource belong to". A namespace has an inode number and can be read through /proc/<pid>/ns/*, so this question can be answered by measurement, not guesswork. The same number means the same namespace, and a different number means a different namespace. That is all there is to it.

Steps

  1. Create the /root/int1 directory and save the value that the /proc/self/ns/pid link points to on the host to /root/int1/host-pid-ns.txt. The file contents must be a single line of pid:[숫자] (the placeholder stands for the namespace inode number).
  2. From the alpine:3.20 image, run a container named dk-ns with --hostname labhub-uts in the background with sleep infinity, and save the /proc/self/ns/pid value read from inside that container to /root/int1/ctr-pid-ns.txt. It must differ from the value in step 1.
  3. Inside dk-ns, print the process list in a format that shows the PID column and save it to /root/int1/pid1.txt (docker exec dk-ns ps -o pid,comm). PID 1 must be sleep — the command you gave when starting that container. The point is that it is neither systemd nor init. On this VM, if you simply run ps -e, PID 1 is systemd, so you get a contrast.
  4. Save the hostname of dk-ns to /root/int1/uts.txt. The contents must be a single line of labhub-uts.
  5. Save this box's /etc/os-release and the alpine image's /etc/os-release appended into one file, /root/int1/mnt-proof.txt. Both the host-side ubuntu and the container-side alpine must appear in the file. Concatenate the output of docker run --rm alpine:3.20 cat /etc/os-release and this VM's same file into one file. Also print uname -r along with them — the kernel is the same and only the file tree differs. The fact that all a mount namespace does is swap a process's / for a different tree is contained in those two lines.
  6. Save /proc/net/dev read inside dk-ns to /root/int1/ctr-net.txt. The lo: line must be present, and the contents must differ from the same file on the host — the interface configuration and the send/receive statistics are managed separately per namespace.
  7. Start a dk-sidecar container in the background that joins the network namespace of dk-ns, and save /proc/self/ns/net read inside dk-ns to /root/int1/ns-a.txt and the same value read inside dk-sidecar to /root/int1/ns-b.txt. Both values must be in the net:[숫자] format (the placeholder stands for the inode number) and equal to each other.
  8. Save the host's /proc/self/uid_map to /root/int1/host-uidmap.txt and the /proc/self/uid_map inside dk-ns to /root/int1/ctr-uidmap.txt. The two mappings must differ from each other.

Notes

Read the host's PID namespace identifier

Create the /root/int1 directory and save the value that the /proc/self/ns/pid link points to on the host to /root/int1/host-pid-ns.txt. The file contents must be a single line of pid:[숫자] (the placeholder stands for the namespace inode number).

Namespace identifiers are exposed as symbolic links under /proc/self/ns/. The string the link points to is the answer itself, so use a command that follows the link, not cat. Only a single line of pid:[숫자] (the placeholder stands for the inode number) should remain in the file.

Compare with the container's PID namespace

From the alpine:3.20 image, run a container named dk-ns with --hostname labhub-uts in the background with sleep infinity, and save the /proc/self/ns/pid value read from inside that container to /root/int1/ctr-pid-ns.txt. It must differ from the value in step 1.

Only if you read the same path inside the container do you get a different number. If you read it in the host shell, you get the same value as step 1 and fail. The container must stay up so you can keep using it in the next steps, so start it with a command that does not exit immediately.

Check PID 1 inside the container

Inside dk-ns, print the process list in a format that shows the PID column and save it to /root/int1/pid1.txt (docker exec dk-ns ps -o pid,comm). PID 1 must be sleep — the command you gave when starting that container. The point is that it is neither systemd nor init. On this VM, if you simply run ps -e, PID 1 is systemd, so you get a contrast.

Print the process list, but in a format that also shows the PID column. The container's main command is process number 1. If you print it on the host, process 1 is a different process.

A hostname separated by the UTS namespace

Save the hostname of dk-ns to /root/int1/uts.txt. The contents must be a single line of labhub-uts.

This step is not about changing the hostname, but about confirming that the hostname you gave when creating the container lives apart from the host's, thanks to the UTS namespace. Ask for the hostname inside the container and save only that value.

The mount namespace — the same kernel, a different file tree

Save this box's /etc/os-release and the alpine image's /etc/os-release appended into one file, /root/int1/mnt-proof.txt. Both the host-side ubuntu and the container-side alpine must appear in the file. Concatenate the output of docker run --rm alpine:3.20 cat /etc/os-release and this VM's same file into one file. Also print uname -r along with them — the kernel is the same and only the file tree differs. The fact that all a mount namespace does is swap a process's / for a different tree is contained in those two lines.

If you include only one side, there is nothing to compare. Leave what you read on the host and what you read inside the container appended in the same file. To append, use an append redirection, not an overwrite.

The interface list of the network namespace

Save /proc/net/dev read inside dk-ns to /root/int1/ctr-net.txt. The lo: line must be present, and the contents must differ from the same file on the host — the interface configuration and the send/receive statistics are managed separately per namespace.

The interface list can also be read through /proc/net/dev (in this environment it is more reliable than ip/ifconfig). If you read it inside the container, you should see only lo and the container interface, with fewer lines than on the host.

Make two containers share a namespace

Start a dk-sidecar container in the background that joins the network namespace of dk-ns, and save /proc/self/ns/net read inside dk-ns to /root/int1/ns-a.txt and the same value read inside dk-sidecar to /root/int1/ns-b.txt. Both values must be in the net:[숫자] format (the placeholder stands for the inode number) and equal to each other.

Instead of creating a new network namespace, there is an option to join someone else's. It is the same as what the pause container does in a Kubernetes Pod. If the join succeeded, the net inode numbers of the two containers must be exactly the same.

UID mappings in the user namespace

Save the host's /proc/self/uid_map to /root/int1/host-uidmap.txt and the /proc/self/uid_map inside dk-ns to /root/int1/ctr-uidmap.txt. The two mappings must differ from each other.

/proc/self/uid_map has three columns, 컨테이너안UID 호스트UID 개수 (the UID inside the container, the UID on the host, and the count). This environment is rootless, so the mapping in the host-side shell and the mapping inside the container come out different — think about why this differs from the measured table in the reading (with Docker's defaults, the user namespace was the same).