Docker & ContainersXXXII Β· Docker InternalsHost-side inspection
Inspecting a container from the host β PIDs, namespaces, cgroups
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
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
$ PID=$(docker inspect grafana --format '{{.State.Pid}}')
echo "$PID"
grep -E '^NStgid|^NSpid' /proc/"$PID"/status2192
NStgid: 2192 1
NSpid: 2192 1NSpid 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
$ cat /proc/2192/cgroup0::/system.slice/docker-25fb9c8b983b3ca3358b9bdc6902921ed5d6e8bc1d78db7eef0e2f5f2645f98a.scopeThat 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.
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.
$ 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:
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)"
donensenter β 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.
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.confThe flags select which namespaces to join:
| Flag | Namespace | Typical use |
|---|---|---|
-n | network | sockets, routes, interfaces, DNS config |
-m | mount | see the containerβs filesystem view |
-p | pid | ps shows the containerβs PID numbering |
-u | UTS | hostname |
-i | IPC | shared memory segments |
-a | all | full 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
Q1. /proc/2192/status shows NSpid: 2192 1. What does that mean?
Q2. top shows PID 8412 consuming twelve cores. How do you find which container it belongs to?
Q3. Why use nsenter rather than docker exec? Select all that apply.
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.