DEV Community

Ryo Tanaka
Ryo Tanaka

Posted on

Run Your First Rootless Container with Boxr (No Daemon, No sudo)

Run an nginx container as an unprivileged user — no daemon, no sudo — and understand what boxr does differently under the hood.

If you know docker run -p 8080:80 nginx, you already know 90% of boxr. Boxr is an OCI container engine written in Rust that runs containers rootless by default: there is no root-owned daemon sitting in the background, no sudo in front of every command, and no setuid helper binaries. Your unprivileged user creates a Linux user namespace, maps the container's root user (UID 0) to your own UID, and the container runs as you.

This is a hands-on tutorial. In about ten minutes you will install boxr, run nginx as a non-root user, look at how the isolation works, switch network modes, exec into the container, and clean up. Boxr is beta software in the 0.1.x series, so expect rough edges — but the core workflow will feel familiar.

What you need

A Linux machine. Rootless containers depend on Linux user namespaces (CLONE_NEWUSER), so this walkthrough is Linux-only. Most mainstream distributions (Ubuntu, Fedora, Debian) enable user namespaces out of the box. Boxr reads /etc/subuid and /etc/subgid to map a range of subordinate user IDs into the container — on a typical single-user workstation those entries already exist for your account.

Install

Two straightforward paths, both documented in the repo README:

Homebrew (Linux or macOS):

brew tap kchaitanya863/tap
brew install boxr
Enter fullscreen mode Exit fullscreen mode

From source with the installer script:

curl -fsSL https://raw.githubusercontent.com/kchaitanya863/boxr/main/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

The script builds boxr with cargo and installs the binary to ~/.boxr/bin — make sure that directory is on your PATH. If you prefer to build by hand, clone the repo and run cargo build --release; the binary lands at target/release/boxr. Native .deb / .rpm packages and Windows builds are also published on the project's release pages.

Verify the install:

boxr --version
Enter fullscreen mode Exit fullscreen mode

Run your first container

boxr run -d --name web -p 8080:80 nginx:latest
Enter fullscreen mode Exit fullscreen mode

Every flag here translates directly from Docker: -d detaches the container into the background, --name web gives it a name, and -p 8080:80 publishes the container's port 80 on host port 8080. Boxr pulls nginx:latest from the registry on first use, exactly like you'd expect.

Confirm it's serving:

curl -s http://localhost:8080 | head -5
Enter fullscreen mode Exit fullscreen mode

You should see the top of nginx's welcome page. Two more commands worth knowing immediately:

boxr ps
boxr logs web
Enter fullscreen mode Exit fullscreen mode

boxr ps lists running containers (it's an alias — boxr ls works too), and boxr logs web streams the container's stdout, which for nginx shows its startup lines.

What "rootless" means here

With Docker, your CLI talks to a root-owned daemon that does the privileged work. Boxr has no daemon. When you run the command above, the boxr process itself:

  1. Forks a small single-threaded helper before its async runtime starts. This matters because the kernel refuses unshare(CLONE_NEWUSER) from a multi-threaded process, so boxr does the namespace setup in a clean one-thread child (the codebase calls this the trampoline).
  2. The child unshares a user namespace; the parent writes your UID and GID mappings for the child's PID.
  3. Inside the namespace, the container's UID 0 is mapped to your unprivileged UID on the host.

The mapping looks like this:

inside the container              on the host
UID 0 (root)            <-->      UID 1000 (you)
UID 1..65536            <-->      100000..165535 (your subuid range)
Enter fullscreen mode Exit fullscreen mode

Everything the container does as "root" is, from the host's perspective, just you. A container escape doesn't hand over host root — it hands over your user account, which is a much smaller blast radius. Port publishing works without privileges too: boxr runs a user-space TCP proxy that forwards host port 8080 into the container's network namespace, so binding low or high ports never needs CAP_NET_BIND_SERVICE.

One practical consequence you'll notice with bind mounts: files the container writes as root show up on the host owned by your user, not by root. That makes this behave sanely:

mkdir -p ~/boxr-data
boxr run -d --name files -v ~/boxr-data:/data:rw alpine sleep 3600
boxr exec files sh -c "echo hello > /data/greeting.txt"
cat ~/boxr-data/greeting.txt
boxr stop files && boxr rm files
Enter fullscreen mode Exit fullscreen mode

No sudo chown dance afterwards.

Choose a network mode

The --network flag (default: auto) controls the container's network setup. These are the real mode names from boxr's source:

  • auto — the default. Uses pasta when the pasta binary is found on your system; without pasta installed, the container runs without an isolated network namespace. If you want the embedded stack for certain, name it explicitly.
  • usernet — boxr's embedded pure-Rust user-mode TCP/IP stack: a TAP device inside the container's network namespace, with ARP, DNS, and TCP handled in-process. Zero external dependencies.
  • pasta — the external passt/pasta rootless networking driver, attached to the container's network namespace. Requires the pasta binary on your PATH.
  • bridge — a boxr-managed bridge network with IPAM address allocation.
  • host — no network isolation; the container shares the host's network namespace.
  • none — an isolated network namespace with loopback only.

Try the embedded stack explicitly — it's the most interesting thing boxr does:

boxr run -d --name web-usernet --network usernet -p 8081:80 nginx:latest
curl -s http://localhost:8081 | head -3
Enter fullscreen mode Exit fullscreen mode

Same nginx, but now every packet crosses a Rust TCP/IP implementation running inside the boxr process: Ethernet frames read from a TAP device, ARP and DNS answered by an in-memory engine, TCP proxied out through ordinary host sockets. If you want the full design — the five-stage packet path, the checksum discipline, and the honest list of what the TCP state machine doesn't do yet — that's covered in the deep-dive linked at the bottom.

Exec into the container

boxr exec web-usernet cat /etc/os-release
Enter fullscreen mode Exit fullscreen mode

boxr exec runs a command inside a running container, the same shape as docker exec. Try poking around: boxr exec web-usernet ls /usr/share/nginx/html shows the files nginx is serving, and boxr top web-usernet lists the processes running in the container.

Troubleshooting the common bumps

operation not permitted when running a container. User namespaces are disabled or restricted. On Debian/Ubuntu check sysctl kernel.unprivileged_userns_clone — it should be 1. Some hardened kernels and containers-within-containers block CLONE_NEWUSER entirely.

Errors about UID/GID mapping. Your user needs a subordinate ID range. Check that /etc/subuid and /etc/subgid contain a line for your username (e.g. you:100000:65536). Most desktop distros create these at install time.

--network auto behaves like host networking. Pasta isn't installed, so auto didn't isolate the network namespace. Either install pasta or pass --network usernet explicitly.

Port already allocated. Another process owns the host port. Pick a free one — -p 8082:80 — and move on.

Clean up

boxr stop web web-usernet
boxr rm web web-usernet
boxr ps
Enter fullscreen mode Exit fullscreen mode

boxr stop halts the containers, boxr rm removes them, and the final boxr ps should show an empty list.

Where to go next

That's the whole loop: install, run, inspect, exec, remove — all as an unprivileged user. From here, the natural next steps are boxr build (Dockerfile-compatible image builds), boxr compose (multi-container apps from a compose file), and boxr volume / boxr network for persistent state and custom networks. The full command reference lives in the repo.

Top comments (0)