# cntr: a docker exec replacement that keeps your own tools inside the container

> cntr is a Rust tool that mounts a running container into a nested namespace so you can use your host's editor and debuggers without installing them into the image. Since version 2.0.0 it uses the Linux mount API rather than FUSE, and it requires kernel 5.2 or later.

**Mic92/cntr** — A container debugging tool based on FUSE

- Repository: https://github.com/Mic92/cntr
- Stars: 957 · Forks: 27
- Language: Rust
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/mic92-cntr

## The problem cntr solves: tools you need but the image does not have

The pitch in the README is one line: say no to installing an editor in your containers. cntr is described as a replacement for docker exec that brings your developer tools with you.

The mechanism is a nested container created with the help of the Linux mount API. Rather than executing a binary inside the target container's own filesystem, cntr mounts the filesystem from one container or from the host into the target, then hands you a shell where both the host tools and the container's files are present. That lets you ship a minimal runtime image in production and keep the attack surface small, while still debugging with the tools you actually use.

The result is a session where your vim, gdb and strace work even if none of them exist in the image, the container's own files are reachable at paths like /var/lib/cntr/etc/ and /var/lib/cntr/var/, and the network, process tree and environment are the container's. The important property is isolation in the other direction: the README states that the original container keeps running normally and that your changes only affect your session, because the mount changes are not visible to container processes.

That last point is what makes this different from simply mounting the container's volume and running a shell. You are not mutating the running container. You are looking at it from a side namespace.

## The 2.0 rewrite dropped FUSE for the Linux mount API

The project description still calls cntr a container debugging tool based on FUSE, and the repository README notes that the academic paper published at USENIX ATC 2018 describes the original FUSE-based architecture. It also states that as of version 2.0.0 cntr was rewritten to use the Linux mount API instead of FUSE, with the claim of significantly improving performance and security while keeping the same user interface and capabilities.

The kernel requirement follows directly from that choice. The README states that Linux kernel 5.2 or later is required because cntr uses the fsopen and fsmount mount API added in 5.2. That is a hard floor, not a soft recommendation. On an older kernel the tool cannot run at all.

The Cargo.toml shows how thin the dependency surface is for a tool that does this. The main library is rustix, pulled in with features covering mount, fs, process, pty, stdio, termios, thread, net, event and pipe, which matches the work of juggling namespaces, pseudo terminals and mount calls. Alongside it sit libc, the log crate, thiserror for error types, and container-pid for resolving a container to a process id. There is no FUSE library in that list any more, which is consistent with the rewrite.

The crate itself is edition 2024 with a rust-version of 1.85, and the single binary target is named cntr at src/bin/main.rs.

## attach mounts the container, exec runs inside it

The tool has two subcommands, and they do different things.

attach gives you a shell in a mount overlay. Cntr mounts the container at /var/lib/cntr, configurable through the CNTR_BASE_DIR environment variable, and the container itself is unaffected because the mounts are invisible to its processes. The command form is cntr attach <container_id>, where the identifier can be a container identifier or a process id. Options include --effective-user for the username used for new files created on the host, and --apparmor with a mode of auto or off, defaulting to auto. The command to run defaults to $SHELL.

exec runs a command from the container filesystem. Its usage is cntr exec <container_id> -- <command>, and the default command is /bin/sh. The reason exec exists as a separate subcommand is layout: container commands often expect their native mount layout at /, so exec chroots into the container and sets up the container's environment.

The help output for both subcommands lists the same container type option, a comma-separated list where the possible values are process-id, podman, docker, nspawn, lxc, lxd, containerd, command and kubernetes, with a default of all but command. That default value is the whole resolution strategy in one line: cntr tries the engines it knows about until one recognises the identifier you gave it.

## Container names for known engines, process ids for everything else

For convenience, cntr supports container names and identifiers natively for a specific list of engines: docker, podman, LXC, LXD, systemd-nspawn, containerd, and k3s via containerd and crictl. For any other engine, the README says cntr also takes process ids instead of container names.

That fallback is the mechanism that makes the tool engine agnostic. A container is ultimately a set of namespaces and a mount tree owned by a process, and the process id is the common denominator across engines the project does not know about. The trade-off is precision. With a name, cntr can ask the engine and get an exact match. With a process id, you are telling it which process to borrow namespaces from, and the README does not document any validation that the pid you pass actually belongs to a container.

The kubernetes value in the type list is worth noting because the description says k3s is supported via containerd and crictl, so the type names are a set of detection strategies rather than interchangeable engines. The process-id type is what you fall back to, and it is also listed first in the possible values.

The repository topics list build-with-buildbot, docker, lxc, rkt and systemd-nspawn, which describes the range of environments the author cared about, and the presence of rkt in the topics but not in the supported type list is the kind of detail worth checking against the source if rkt is what you run.

## Installing cntr from cargo, a release tarball, or the Dockerfile

Cntr runs only on Linux by design. The simplest install is cargo, since all you need for compilation is rust and cargo:

```console
$ cargo install cntr
```

To follow the master branch instead of the published crate:

```console
$ cargo install --git https://github.com/Mic92/cntr
```

For linux x86_64 the project builds static binaries for every release, and more platforms can be added on request, with pre-built tarballs on the releases tab. At runtime the only requirement is the command line utilities of the container engine in question. The README also mentions an offline tarball with all dependencies bundled for compilation with cargo-vendor, which matters for air-gapped build machines, and points distribution packagers at PACKAGING.md.

The Dockerfile in the repository shows a slim and fat container setup: a build stage based on rust:1.98.1 that adds the musl target, and a busybox stage that copies the stripped binary and the docker-pid helper into place. The comments at the end of the file show how it is meant to be run, including the privileged flag and the mounted Docker socket:

```console
$ sudo docker run --pid=host --privileged=true -v /var/run/docker.sock:/var/run/docker.sock -ti --rm cntr attach mycontainer /bin/sh
```

The Makefile offers a conventional path too, building with cargo build --release and installing to $(DESTDIR)/usr/local/bin.

## Where cntr cannot help

Three boundaries are stated plainly, and each one rules out a real use case.

The first is the kernel. Below 5.2 there is no fsopen and fsmount API to call, so cntr does not start. The second is host locality: the README states that cntr needs to run on the same host as the container and does not work if the container is running in a virtual machine while cntr runs on the hypervisor. Docker-in-Docker or a Kubernetes node boundary both violate that, because you cannot join namespaces across a virtualisation boundary.

The third is privilege. cntr creates mounts and enters the namespaces of another process, which means it needs the kind of access that the Dockerfile example gets with --privileged and the mounted Docker socket. On a shared build host where your user cannot manage containers at the host level, attach is not going to work.

There is also a quieter limitation. The tooling support is listed for docker, podman, LXC, LXD, systemd-nspawn, containerd and k3s. For anything else, you pass a process id, and the README gives no guidance on picking the right one or on what happens if you pick wrong.

Finally, cntr is a debugging convenience, not a production mechanism. It exists to make one session more convenient; nothing in the documentation suggests it is meant to be a permanent part of a deployment.

## How cntr differs from docker exec and from nsenter

The everyday alternative is docker exec, which runs a binary that must exist inside the image. That is the difference cntr is built around: docker exec is limited to what the image ships, cntr is not, because the tools come from the host and are mounted in.

The closer alternative is nsenter, which shares the namespaces of a running process. Its disadvantage is exactly what cntr addresses: the tools are still not there. nsenter gives you the container's view, not your host's tools.

cntr sits between the two. It pays for that with a kernel version floor, a host locality constraint and a privilege requirement. For the case the author had in mind, slim production images plus a developer who wants vim and gdb on the machine, the trade is worth it. For a quick one-off look at a process, nsenter is less ceremony.

The project also publishes a Nix flake, a nix package definition and a vm-test.nix file, plus completions and shell-completion tests, which suggests the maintainer treats it as a tool expected to be installed on developer workstations rather than shipped inside images.

## Conclusion

cntr fits teams that ship slim production images but still want vim, gdb or strace on the box while debugging, on a Linux host where the container runs in the same kernel namespace. It is the wrong choice on kernels older than 5.2, on a machine where the container lives inside a virtual machine, or when you need to attach to a container you do not have host privileges for, since the tool runs mounts and shares namespaces with the target. Before committing, verify two things yourself: that your kernel exposes the fsopen and fsmount calls cntr depends on, and that the container engine on your host is one of the types cntr can resolve by name, docker, podman, LXC, LXD, systemd-nspawn, containerd or k3s, otherwise you will be passing raw process ids.

## FAQ

### What kernel version does cntr need?

Linux kernel 5.2 or later, because cntr uses the fsopen and fsmount mount API added in 5.2. The README states that cntr is only able to run on Linux at all.

### How do I attach cntr to a running container?

Use cntr attach <container_id>, where the identifier can be a container identifier or a process id. The container is mounted at /var/lib/cntr and can be changed with the CNTR_BASE_DIR environment variable.

### What is the difference between cntr attach and cntr exec?

attach gives you a shell in a mount overlay where your host tools and the container files are both visible, while the container keeps running unaffected. exec runs a command from the container filesystem and chroots into the container, because container commands expect their native mount layout at /.

### Which container engines can cntr attach to?

Docker, podman, LXC, LXD, systemd-nspawn, containerd and k3s via containerd and crictl are supported natively by name or identifier. For other engines, cntr takes a process id instead of a container name.

### Does cntr still use FUSE?

No. The README states that as of version 2.0.0 cntr was rewritten to use the Linux mount API instead of FUSE, while keeping the same user interface. The academic paper published at USENIX ATC 2018 describes the earlier FUSE-based architecture.

## Sources

- [Issues](https://github.com/Mic92/cntr/issues)
- [License: MIT](https://github.com/Mic92/cntr/blob/main/LICENSE)
- [Mic92/cntr on GitHub](https://github.com/Mic92/cntr)
- [README](https://github.com/Mic92/cntr/blob/main/README.md)
- [Releases](https://github.com/Mic92/cntr/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mic92-cntr
