Fix Container Breaks Out of Namespace Isolation on Linux
Your container can see host processes or files? That's a namespace isolation failure. Here's how to lock it down fast.
Quick answer
If your container can see host processes (like running ps aux inside shows everything), you're missing the correct namespace flags. The fix: docker run --pid=host ... is wrong. Use --pid=container:... wait no—skip all that. The real fix is to run with --security-opt no-new-privileges and --cap-drop=ALL, then add back only what you need. But the core issue? The container runtime didn't create new namespaces. Let's fix that.
Why this happens
Linux containers work because of namespaces. They're like walls between the container and the host. When those walls are missing—broken because you didn't ask for them, or your runtime default is too loose—the container shares the same process tree, network, or mount points as the host. I've seen this on Docker versions before 20.10.7, and it's still common on older Podman installs. Real-world trigger: you copy a Docker Compose file from some tutorial from 2019 that used pid: host without thinking, or you used docker run --privileged because it was easier. That kills isolation.
Numbered fix steps
- Check your runtime version. Run
docker --versionorpodman --version. If you're on Docker older than 20.10, upgrade. Old versions had bugs where namespaces didn't always apply. On Ubuntu:sudo apt update && sudo apt install docker-ce docker-ce-cli containerd.io. - Drop all capabilities first. When you run your container, start with
--cap-drop=ALL. Then add back only what your app needs. Example for a web server:--cap-add=NET_BIND_SERVICE(port 80). This forces the container into a restricted namespace. Command:docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE --security-opt no-new-privileges -d nginx - Explicitly create a PID namespace. By default, Docker and Podman create a new PID namespace. But if you used
--pid=hostin your config, remove it. In Docker Compose, that line looks likepid: host. Delete it. Then run:docker run --pid=container:yourcontainer ...– no, that links two containers. Just omit--pidentirely. Good. - Verify isolation. Inside the container, run
ps aux. If you see only a few processes (likenginxandbash), you're good. If you see 200 processes including systemd, the fix didn't work. Go to step 5. - Check mount namespace. Sometimes the issue is that the container sees host filesystems. Run
docker inspect container_name | grep -i mount. If you see"Propagation": "shared", that's your problem. Change to"private". In Docker run, add--mount type=bind,source=/data,target=/data,bind-propagation=private.
Alternative fixes if the main one fails
- Switch to Podman. Podman defaults to rootless containers, which use user namespaces by default. This is a stronger isolation than Docker's default rootful mode. Install:
sudo apt install podman(Ubuntu) orsudo dnf install podman(Fedora). Then run your container withpodman run .... - Set
--userns=host? No, wrong direction. You want--userns=keep-idfor rootless, or just don't use--usernsat all. The default is fine. - If you're using Docker Compose, add
userns_mode: "host"? No, that's the same problem. Remove anyuserns_modelines. - Reboot your host. I know, it's stupid. But I've seen a kernel module stuck after a container crash that prevented new namespaces. Run
sudo rebootand try again.
Prevention tip
Never run containers as --privileged. That flag disables all namespace isolation. Instead, use --cap-drop=ALL and add only the capabilities you need. Also, set your Docker daemon to use user namespaces globally. On Ubuntu, edit /etc/docker/daemon.json and add:
{
"userns-remap": "default"
}
Then restart Docker: sudo systemctl restart docker. This remaps container users to non-root users on the host, adding another layer. Finally, test your containers with a tool like container-diff or grype for vulnerabilities, but that's another story.
One last thing: if you're on Podman, you're already in a safer place. But still, check your containers.conf file. Make sure no_new_privileges = true and cap_drop = ["ALL"] are set. That's it.
Was this solution helpful?