Skip to main content
RunBook Academy

Docker & ContainersXXXII Β· Docker InternalsHost-side inspection

Inspecting a container from the host β€” PIDs, namespaces, cgroups

Expert⏱ ~24 min

What you'll learn

  • Map a container ID to a host PID, its namespaces and its cgroup, in both directions
  • Use nsenter to run host tooling inside a container namespace
  • Identify which host process belongs to which container from a PID alone
  • Diagnose containers whose image contains no shell

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11

Not yet marked complete on this device.

A container is not an object the kernel knows about. It is a process, a set of namespace memberships, and a cgroup. Every question you can ask about a container is really a question about one of those three, and all three are readable from the host with tools that have nothing to do with Docker.

That matters twice: when the image has no shell to exec into, and when the daemon itself is the thing that is broken.

The anchor: the host PID

Read-only / Safeone process, two numbers
$ PID=$(docker inspect grafana --format '{{.State.Pid}}')
echo "$PID"
grep -E '^NStgid|^NSpid' /proc/"$PID"/status
2192
NStgid:	2192	1
NSpid:	2192	1

NSpid lists the process’s ID in each PID namespace it belongs to, outermost first. Two columns means one level of nesting: the host, then the container.

This is the single most useful line in this lesson, because it explains a whole class of confusion. A log message saying β€œkilling pid 1” refers to 2192 on the host. kill 1 inside the container and kill 2192 on the host do the same thing. top on the host shows 2192 and no PID 1 for that workload anywhere.

The cgroup, in both directions

Read-only / SafePID to container
$ cat /proc/2192/cgroup
0::/system.slice/docker-25fb9c8b983b3ca3358b9bdc6902921ed5d6e8bc1d78db7eef0e2f5f2645f98a.scope

That is the reverse mapping, and it is the one you need during an incident. top shows a process eating twelve cores; you have a PID and nothing else.

Read-only / Safewhich container is this PID
PID=2192
CID=$(grep -o 'docker-[0-9a-f]\{64\}' /proc/"$PID"/cgroup | head -1 | cut -d- -f2)
docker inspect "$CID" --format '{{.Name}} image={{.Config.Image}} started={{.State.StartedAt}}'

The same trick works from the other side. Given a container, list every host process inside it:

docker top grafana

docker top runs ps on the host and filters to the container’s processes, so it works even when the image has no ps.

Namespaces are inodes

Each namespace is a file in /proc/<pid>/ns/, and the link target carries an inode number. Two processes are in the same namespace if and only if the inode matches.

Read-only / Safea host shell
$ ls -l /proc/self/ns/
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 mnt -> mnt:[4026531832]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 net -> net:[4026531833]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 pid -> pid:[4026531836]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 time -> time:[4026531834]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 user -> user:[4026531837]
lrwxrwxrwx 1 ebrandi ebrandi 0 Aug 11 15:54 uts -> uts:[4026531838]

Comparing the two sides answers questions that are otherwise guesswork:

Read-only / Safewhat is actually isolated
PID=$(docker inspect grafana --format '{{.State.Pid}}')
for ns in cgroup ipc mnt net pid user uts; do
printf '%-7s host=%s container=%s\n' "$ns" \
  "$(sudo readlink /proc/1/ns/$ns)" \
  "$(sudo readlink /proc/"$PID"/ns/$ns)"
done

nsenter β€” host tools inside a container

docker exec runs a binary from the image. On a distroless or scratch image there is no shell, no ss, no ip, no curl, and docker exec has nothing to run.

nsenter runs a binary from the host inside the container’s namespaces. The container needs to contain nothing at all.

Read-only / Safedebug a distroless container
PID=$(docker inspect api --format '{{.State.Pid}}')
sudo nsenter -t "$PID" -n ss -tlnp
sudo nsenter -t "$PID" -n ip -brief addr
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n cat /etc/resolv.conf

The flags select which namespaces to join:

FlagNamespaceTypical use
-nnetworksockets, routes, interfaces, DNS config
-mmountsee the container’s filesystem view
-ppidps shows the container’s PID numbering
-uUTShostname
-iIPCshared memory segments
-aallfull docker exec equivalent, with host binaries

That last line of the example is worth a note: cat /etc/resolv.conf under -n alone reads the host’s file, because you joined the network namespace but not the mount namespace. Add -m to see the container’s copy. Mixing these up produces confident wrong answers during DNS debugging.

Sanity check

Knowledge check Β· 4 questions

  1. Q1. /proc/2192/status shows NSpid: 2192 1. What does that mean?

  2. Q2. top shows PID 8412 consuming twelve cores. How do you find which container it belongs to?

  3. Q3. Why use nsenter rather than docker exec? Select all that apply.

  4. Q4. A container started without user-namespace remapping shares the host user namespace, so its root is the host root.

Passing score: 75%. Answers are checked in this browser.