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
From source with the installer script:
curl -fsSL https://raw.githubusercontent.com/kchaitanya863/boxr/main/install.sh | sh
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
Run your first container
boxr run -d --name web -p 8080:80 nginx:latest
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
You should see the top of nginx's welcome page. Two more commands worth knowing immediately:
boxr ps
boxr logs web
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:
- 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). - The child unshares a user namespace; the parent writes your UID and GID mappings for the child's PID.
- 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)
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
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 thepastabinary on yourPATH. -
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
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
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
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.
- Repository: https://github.com/kchaitanya863/boxr — README, architecture docs, and the CLI reference.
- Deep-dive: Why I Put a TCP/IP Stack Inside a Rootless Container Engine — https://dev.to/ryo_tanaka_dev/why-i-put-a-tcpip-stack-inside-a-rootless-container-engine-8c — the UserNet design this tutorial only skimmed.
- Discussions: https://github.com/kchaitanya863/boxr/discussions — design threads and Q&A. Boxr is beta and single-maintainer, and the most useful thing you can do after this tutorial is run something unusual in it and report what breaks.
Top comments (0)