How do you enter a running Docker container's namespaces with nsenter from the engine host?
answer
- A container is only host processes
- The engine records a host-visible PID
- setns joins one namespace per flag
- Skipping the mount namespace keeps your tools
- Container files live under /proc/PID/root
basics
~20 sRead the container's host PID with docker inspect -f '{{.State.Pid}}', then as root on the engine host run nsenter -t <pid> -n -p. Omitting -m keeps the host's mount namespace, so host tools still work while you see the container's network and processes.
solid answer
~50 sA container is just host processes with namespaces and a cgroup, so you can join those namespaces directly. `docker inspect -f '{{.State.Pid}}' billing-cron` returns the host PID of the container's PID 1 — 24817, say, and 0 if the container is not running. Then, as root on the machine running `dockerd`, `nsenter -t 24817 -n -p -- ps -ef` enters that container's network and PID namespaces. The critical choice is `-m`: **omit it** and you keep the host's mount namespace, so the host's `ss`, `strace`, `lsof` and `ls` are all still on your PATH while you look at the container's network and processes — that is exactly what makes this work on a shell-less image. Add `-m` and you get the container's own filesystem back, tools and all missing again. Without `-m` the container's files are still reachable at `/proc/24817/root`.
code
bash · 9 linesPID=$(docker inspect -f '{{.State.Pid}}' billing-cron) # e.g. 24817, or 0 if not running
# container's network + process view, HOST tools (no -m)
sudo nsenter -t "$PID" -n -p -- ps -ef
sudo nsenter -t "$PID" -n -- ss -ltnp
# the container's files, still with host tools
sudo ls -l "/proc/$PID/root/etc/"
sudo tr '\0' '\n' < "/proc/$PID/environ"go deeper
Know the two-step shape: get the container's host PID from docker inspect, then use nsenter on the host to look inside. You are not expected to recall every namespace flag yet.
Explain what setns does and what each flag joins, and above all why omitting -m is the point: keeping the host's mount namespace is what leaves you with working tools on a shell-less image.
Demonstrate the operational judgement: check State.Pid is non-zero, prefer reads over changes on a live production container, and know that this path needs host root and is therefore root-equivalent over every container on that engine.
Own the access model. Deciding who may enter namespaces on a production engine host, whether that is audited, and what the sanctioned alternative is for teams without host access, is a platform-level policy choice, not a debugging preference.
### The premise There is no such thing as "inside the container" at the kernel level. A container is a set of ordinary Linux processes that have been given their own namespaces (mount, network, PID, UTS, IPC, and optionally user and cgroup) and placed in a cgroup. `nsenter` is a plain userspace tool that calls `setns()` to attach the calling process to another process's namespaces. Because it runs from the **host**, it does not care what the container's image contains — which is precisely why it is the standard answer for an image with no shell. ### Finding the target The engine records the host-visible PID of the container's PID 1: ``` $ docker inspect -f '{{.State.Pid}}' billing-cron 24817 ``` Two things to know about this value. It is a PID **in the host's PID namespace**, so it is the number you see in the host's `ps`, not the `1` the process sees itself as. And it is `0` when the container is not running — if `inspect` prints `0`, there are no namespaces left to enter and you are in a post-mortem, not a live debug. ### The command and its flags ``` sudo nsenter -t 24817 -n -p -- ps -ef ``` `-t` (`--target`) names the process whose namespaces you want. The rest select which namespaces to join, one flag each: `-m` mount, `-n` net, `-p` PID, `-u` UTS (hostname), `-i` IPC, `-U` user, `-C` cgroup, and `-a` for all of them. Anything after `--` is the command to run; with no command, `nsenter` runs your `$SHELL`. `nsenter` needs privilege — practically, root on the engine host, because `setns()` into another process's namespaces requires `CAP_SYS_ADMIN` in the owning user namespace. Anyone who can do this on the host is already root-equivalent on every container on it; that is a fact worth stating in an interview, because it is why this route is an operator's tool and not a developer's. ### The `-m` trap, which is the whole point The mount namespace is what makes the container's filesystem *be the image*. So: * **Without `-m`**, your new process keeps the **host's** mount namespace. Your PATH resolves to host binaries, so `ps`, `ss`, `strace`, `lsof`, `tcpdump` and `cat` all exist and run — while `-n` and `-p` mean they are looking at the container's network stack and process tree. On a `FROM scratch` billing-cron container this is the only way to get `strace` anywhere near the process. * **With `-m`**, you enter the container's mount namespace too, your PATH now resolves inside the image, and you are back to "no such file or directory" for every tool. Candidates who have only ever copy-pasted `nsenter -t $PID -m -u -i -n -p` from a blog hit this and conclude nsenter is broken. The container's files are still reachable without `-m`, through the magic symlink `/proc/24817/root`, which resolves paths as if that process's root were the root: ``` ls -l /proc/24817/root/etc/ cat /proc/24817/environ | tr '\0' '\n' cat /proc/24817/cgroup ``` That gives you the container's config files, environment, cgroup path and open file descriptors (`/proc/24817/fd`) with host tools. ### Practical notes With `-p`, `nsenter` **forks**: a process cannot change its own PID namespace, only its children can be created in the new one, so the shell you get is a child. That is why the entered shell shows itself with a small PID and why `nsenter -p` without a command still behaves sanely. Entering only `-n` is a common narrower choice when you want the container's network view with host tooling and do not care about its process tree. On **Docker Desktop** there is no host PID to target: `dockerd` runs inside a Linux VM, so `State.Pid` is a PID in that VM's namespace and `nsenter` must run in the VM — usually via a privileged container started with `--pid=host`, which puts you in the VM's PID namespace first. Docker Desktop also ships a `docker debug` command that packages this whole trick as a supported subcommand; it is a paid-subscription feature, so do not assume it is present. ### When not to reach for it If you only have the Docker API and not root on the engine host — the normal case for a developer against a shared or remote engine — `nsenter` is not available to you at all, and the toolbox-container route is the answer instead. Saying that out loud is what separates a memorised command from understanding the access model.
- You ran nsenter with -m and every command reports 'No such file or directory'. What happened?`-m` joined the container's mount namespace, so path lookups now happen in the image's filesystem, which has no `ps`, no `ls` and no shell. Drop `-m`: with only `-n -p` you keep the host's mount namespace and therefore the host's binaries, while still seeing the container's network stack and process tree. Reach the container's files through `/proc/<pid>/root` instead.
- docker inspect prints State.Pid as 0. What does that tell you?The container is not running, so the engine has no host PID for it. Its namespaces have been torn down and there is nothing left to enter — `nsenter` and namespace-joining containers are both off the table. You are now doing post-mortem work: the exit state recorded by the engine, the container's logs, and its filesystem contents rather than live process inspection.
- You are on macOS with Docker Desktop and no such PID exists on your machine. What now?The daemon runs inside a Linux VM, so `State.Pid` is a PID in that VM. You must be in the VM first — commonly by starting a privileged container with `--pid=host`, which places you in the VM's PID namespace, and running `nsenter` from there. Docker Desktop's own `docker debug` command wraps this, but it is a subscription feature rather than something you can rely on.
- Who is allowed to run nsenter against a container, and why does that matter?Effectively only root on the engine host, because joining another process's namespaces needs `CAP_SYS_ADMIN`. That access is root-equivalent over every container on the machine, so it belongs to operators, is worth auditing, and is not something you can hand to an application team as a routine debugging workflow. Developers with only Docker API access need the namespace-joining container route instead.
saying these in an interview costs you the question
- Passes the container ID to nsenter -t instead of a PID
- Thinks nsenter runs inside the container's filesystem
- Always copies -m and blames nsenter when tools vanish
- Believes nsenter works without root on the host
- Assumes State.Pid is the PID the process sees itself as
- Expects a host PID to exist under Docker Desktop on macOS