<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ryo Tanaka</title>
    <description>The latest articles on DEV Community by Ryo Tanaka (@ryo_tanaka_dev).</description>
    <link>https://dev.to/ryo_tanaka_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143800%2F202fe8b1-0ed3-4ebd-9094-51ab50e51eab.png</url>
      <title>DEV Community: Ryo Tanaka</title>
      <link>https://dev.to/ryo_tanaka_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ryo_tanaka_dev"/>
    <language>en</language>
    <item>
      <title>I benchmarked boxr vs podman on container startup. Here are the honest numbers.</title>
      <dc:creator>Ryo Tanaka</dc:creator>
      <pubDate>Sat, 26 Sep 2026 05:54:02 +0000</pubDate>
      <link>https://dev.to/ryo_tanaka_dev/i-benchmarked-boxr-vs-podman-on-container-startup-here-are-the-honest-numbers-4ikc</link>
      <guid>https://dev.to/ryo_tanaka_dev/i-benchmarked-boxr-vs-podman-on-container-startup-here-are-the-honest-numbers-4ikc</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; On the same VM, starting a minimal cached container 20 times each: &lt;strong&gt;boxr 0.1.44 median 134 ms, podman 4.9.3 median 217 ms.&lt;/strong&gt; boxr was ~38% faster on median startup. The full methodology and every caveat are below — please read them before quoting these numbers, because the caveats matter more than the headline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;The simplest meaningful container operation: start a container from an already-pulled image and exit immediately.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;boxr: &lt;code&gt;boxr run --rootless --rm --network none library/alpine:latest true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;podman: &lt;code&gt;podman run --rm --network none docker.io/library/alpine:latest true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same image bytes on both sides (alpine, identical config digest &lt;code&gt;sha256:320994c3…&lt;/code&gt;). Wall-clock time measured with &lt;code&gt;date +%s%N&lt;/code&gt; around the full CLI invocation — that's the cost a user actually feels, including CLI startup. Five cold runs per engine, then 20 warm runs each, alternating boxr/podman per iteration so any drift in machine load hits both equally. Zero failed runs out of 50, and I kept every data point — no outliers removed.&lt;/p&gt;

&lt;p&gt;Warm results (n=20 each):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;min&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;boxr 0.1.44&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;134 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;150 ms&lt;/td&gt;
&lt;td&gt;107 ms&lt;/td&gt;
&lt;td&gt;262 ms&lt;/td&gt;
&lt;td&gt;207 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;podman 4.9.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;217 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;225 ms&lt;/td&gt;
&lt;td&gt;181 ms&lt;/td&gt;
&lt;td&gt;310 ms&lt;/td&gt;
&lt;td&gt;288 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cold runs (first 5 invocations, image already pulled): boxr median 128 ms, podman median 212 ms — same story.&lt;/p&gt;

&lt;p&gt;Environment: 2 vCPU (AMD EPYC 9D64), 7.7 GiB RAM, Ubuntu 24.04.5, kernel 7.0.0-38-generic. boxr was the prebuilt v0.1.44 Linux x86_64 release binary (checksum-verified). Podman came from Ubuntu's packages with crun 1.14.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caveats (please don't skip these)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is not a "rootless podman" measurement.&lt;/strong&gt; I wanted both engines rootless, but this VM is itself a user-namespaced container with restrictions that made unprivileged user namespaces unusable (unprivileged &lt;code&gt;uid_map&lt;/code&gt; writes fail, and setuid helpers can't elevate under the inherited no-new-privs flag — I verified this several ways before giving up). So both engines ran as root. boxr ran in its &lt;code&gt;--rootless&lt;/code&gt; mode; podman ran rootful, without a container user namespace. If anything, that flatters podman: it skipped the user-namespace setup that boxr performed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Networking was disabled on both sides.&lt;/strong&gt; Podman's default netavark networking physically cannot run in this environment (&lt;code&gt;setns: Operation not permitted&lt;/code&gt;), so podman &lt;em&gt;required&lt;/em&gt; &lt;code&gt;--network none&lt;/code&gt;. I gave boxr &lt;code&gt;--network none&lt;/code&gt; too, for symmetry — its default &lt;code&gt;auto&lt;/code&gt; mode sets up pasta networking and I measured that at roughly +45 ms. So: this compares engine + runtime startup, not network-stack setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Podman needed environment workarounds.&lt;/strong&gt; Default podman storage chose fuse-overlayfs (no &lt;code&gt;/dev/fuse&lt;/code&gt; here), so I pointed its graphroot at tmpfs, which lets it use native overlay with idmapped mounts — that likely &lt;em&gt;helped&lt;/em&gt; podman, not hurt it. I also used &lt;code&gt;--cgroup-manager=cgroupfs&lt;/code&gt; because there's no D-Bus here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One VM, one image, one workload.&lt;/strong&gt; n=20 is enough to see the gap is real (the distributions barely overlap — boxr's max warm run was 262 ms, podman's min was 181 ms), but it's still one machine and &lt;code&gt;true&lt;/code&gt; is the most trivial workload possible. Real workloads with volume mounts, port publishing, and actual processes will look different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A boxr quirk I had to work around:&lt;/strong&gt; boxr doesn't normalize the &lt;code&gt;docker.io/&lt;/code&gt; registry prefix, so &lt;code&gt;docker.io/library/alpine:latest&lt;/code&gt; misses its image cache and re-pulls + re-extracts on &lt;em&gt;every&lt;/em&gt; run (~1.7 s penalty each time). I used the &lt;code&gt;library/alpine:latest&lt;/code&gt; spelling boxr's cache recognizes. Same image, but worth knowing — I've written it up as a bug report for the maintainer to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think the numbers mean (speculation — labeled as such)
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Speculation starts here.&lt;/em&gt; The ~80 ms gap is consistent with architectural differences rather than tuning: podman is a Go CLI that shells out through conmon to crun with a fair amount of configuration plumbing per invocation, while boxr is a single Rust binary with a shorter path from CLI to runtime. I'd guess CLI startup + per-run setup dominates at this scale, not the OCI runtime itself (both ultimately drive containers the same way).&lt;/p&gt;

&lt;p&gt;What would change the picture: a heavier workload would shrink the &lt;em&gt;relative&lt;/em&gt; gap (fixed startup cost amortized over real work); podman's daemonless-but-heavier CLI is a per-invocation tax, so short-lived containers are exactly where boxr's design should win and long-running ones where it matters least. I haven't measured any of that — it's inference, not data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where startup latency actually matters (and where it doesn't)
&lt;/h2&gt;

&lt;p&gt;For long-running services, 80 ms is noise — nobody picks a container engine over a tenth of a second at deploy time. Startup latency matters in a narrower but real set of cases: CI jobs that spin up one-shot containers per test step, &lt;code&gt;--rm&lt;/code&gt; helper containers in scripts, local dev loops where you restart a container dozens of times a day, and anything resembling scale-to-zero. If your containers live for hours, stop reading and go tune something else. If you launch hundreds of short-lived ones a day, per-invocation overhead is one of the few engine differences you'll actually feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd measure next
&lt;/h2&gt;

&lt;p&gt;This benchmark answered one narrow question. The honest follow-ups, in order of how much they'd change my confidence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;As a non-root user.&lt;/strong&gt; The single biggest gap in this data. True rootless podman (user namespaces, pasta/slirp networking, fuse-overlayfs) has a meaningfully different startup path than the rootful podman I could run here. If someone reruns this on bare metal as an unprivileged user, that's the comparison I actually wanted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real workload.&lt;/strong&gt; &lt;code&gt;true&lt;/code&gt; measures engine overhead and nothing else. Next step up: a container that starts a web server and serves one request, timed to first byte. That folds in network setup — where the engines genuinely differ — instead of excluding it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold page cache.&lt;/strong&gt; Everything here was page-cache warm. Dropping caches between runs (&lt;code&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;) would show the storage-driver side of the story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More engines.&lt;/strong&gt; Docker and nerdctl belong in this comparison before any strong claims.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Pull alpine once in each engine, then time &lt;code&gt;run --rm &amp;lt;image&amp;gt; true&lt;/code&gt; in a loop, alternating engines. If you run this on your own machine — especially as a non-root user, which I couldn't do here — I'd genuinely like to see your numbers. The methodology that matters: same image digest, warmed cache, no cherry-picking, report the setup you actually used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;boxr repo: &lt;a href="https://github.com/kchaitanya863/boxr" rel="noopener noreferrer"&gt;https://github.com/kchaitanya863/boxr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discussion (questions, your own benchmark numbers welcome): &lt;a href="https://github.com/kchaitanya863/boxr/discussions/397" rel="noopener noreferrer"&gt;https://github.com/kchaitanya863/boxr/discussions/397&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you rerun this and get different results, that's useful data, not an argument — post them in the discussion and I'll update this article.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>containers</category>
      <category>docker</category>
      <category>performance</category>
    </item>
    <item>
      <title>Run Your First Rootless Container with Boxr (No Daemon, No sudo)</title>
      <dc:creator>Ryo Tanaka</dc:creator>
      <pubDate>Sat, 26 Sep 2026 05:35:38 +0000</pubDate>
      <link>https://dev.to/ryo_tanaka_dev/run-your-first-rootless-container-with-boxr-no-daemon-no-sudo-56aj</link>
      <guid>https://dev.to/ryo_tanaka_dev/run-your-first-rootless-container-with-boxr-no-daemon-no-sudo-56aj</guid>
      <description>&lt;p&gt;&lt;em&gt;Run an nginx container as an unprivileged user — no daemon, no sudo — and understand what boxr does differently under the hood.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you know &lt;code&gt;docker run -p 8080:80 nginx&lt;/code&gt;, 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 &lt;code&gt;sudo&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Two straightforward paths, both documented in the repo README:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Homebrew (Linux or macOS):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap kchaitanya863/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;boxr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From source with the installer script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/kchaitanya863/boxr/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Verify the install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boxr &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run your first container
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boxr run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; web &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Confirm it's serving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the top of nginx's welcome page. Two more commands worth knowing immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boxr ps
boxr logs web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  What "rootless" means here
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;The mapping looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inside the container              on the host
UID 0 (root)            &amp;lt;--&amp;gt;      UID 1000 (you)
UID 1..65536            &amp;lt;--&amp;gt;      100000..165535 (your subuid range)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;CAP_NET_BIND_SERVICE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/boxr-data
boxr run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; files &lt;span class="nt"&gt;-v&lt;/span&gt; ~/boxr-data:/data:rw alpine &lt;span class="nb"&gt;sleep &lt;/span&gt;3600
boxr &lt;span class="nb"&gt;exec &lt;/span&gt;files sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo hello &amp;gt; /data/greeting.txt"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/boxr-data/greeting.txt
boxr stop files &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; boxr &lt;span class="nb"&gt;rm &lt;/span&gt;files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;sudo chown&lt;/code&gt; dance afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a network mode
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;--network&lt;/code&gt; flag (default: &lt;code&gt;auto&lt;/code&gt;) controls the container's network setup. These are the real mode names from boxr's source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;auto&lt;/code&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usernet&lt;/code&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pasta&lt;/code&gt; — the external passt/pasta rootless networking driver, attached to the container's network namespace. Requires the &lt;code&gt;pasta&lt;/code&gt; binary on your &lt;code&gt;PATH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bridge&lt;/code&gt; — a boxr-managed bridge network with IPAM address allocation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host&lt;/code&gt; — no network isolation; the container shares the host's network namespace.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; — an isolated network namespace with loopback only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try the embedded stack explicitly — it's the most interesting thing boxr does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boxr run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; web-usernet &lt;span class="nt"&gt;--network&lt;/span&gt; usernet &lt;span class="nt"&gt;-p&lt;/span&gt; 8081:80 nginx:latest
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8081 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exec into the container
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boxr &lt;span class="nb"&gt;exec &lt;/span&gt;web-usernet &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Troubleshooting the common bumps
&lt;/h2&gt;

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

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

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--network auto&lt;/code&gt; behaves like host networking.&lt;/strong&gt; Pasta isn't installed, so &lt;code&gt;auto&lt;/code&gt; didn't isolate the network namespace. Either install pasta or pass &lt;code&gt;--network usernet&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port already allocated.&lt;/strong&gt; Another process owns the host port. Pick a free one — &lt;code&gt;-p 8082:80&lt;/code&gt; — and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boxr stop web web-usernet
boxr &lt;span class="nb"&gt;rm &lt;/span&gt;web web-usernet
boxr ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;boxr stop&lt;/code&gt; halts the containers, &lt;code&gt;boxr rm&lt;/code&gt; removes them, and the final &lt;code&gt;boxr ps&lt;/code&gt; should show an empty list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/kchaitanya863/boxr" rel="noopener noreferrer"&gt;https://github.com/kchaitanya863/boxr&lt;/a&gt; — README, architecture docs, and the CLI reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-dive:&lt;/strong&gt; &lt;em&gt;Why I Put a TCP/IP Stack Inside a Rootless Container Engine&lt;/em&gt; — &lt;a href="https://dev.to/ryo_tanaka_dev/why-i-put-a-tcpip-stack-inside-a-rootless-container-engine-8c"&gt;https://dev.to/ryo_tanaka_dev/why-i-put-a-tcpip-stack-inside-a-rootless-container-engine-8c&lt;/a&gt; — the UserNet design this tutorial only skimmed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a href="https://github.com/kchaitanya863/boxr/discussions" rel="noopener noreferrer"&gt;https://github.com/kchaitanya863/boxr/discussions&lt;/a&gt; — design threads and Q&amp;amp;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.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>containers</category>
      <category>docker</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why I Put a TCP/IP Stack Inside a Rootless Container Engine</title>
      <dc:creator>Ryo Tanaka</dc:creator>
      <pubDate>Sat, 26 Sep 2026 05:13:48 +0000</pubDate>
      <link>https://dev.to/ryo_tanaka_dev/why-i-put-a-tcpip-stack-inside-a-rootless-container-engine-8c</link>
      <guid>https://dev.to/ryo_tanaka_dev/why-i-put-a-tcpip-stack-inside-a-rootless-container-engine-8c</guid>
      <description>&lt;p&gt;&lt;em&gt;Inside Boxr’s pure-Rust UserNet: TAP frames, ARP, DNS, TCP proxying, and the trade-offs that keep it beta.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rootless containers have a networking problem that looks simple until you follow the packet.&lt;/p&gt;

&lt;p&gt;Creating an isolated network namespace is only the first step. The application inside the container still expects DNS, outbound TCP, port forwarding, and behavior close enough to a normal Linux host that tools such as curl or a package manager do not care how the connection was built. Meanwhile, the runtime cannot rely on host root privileges to create and configure everything the traditional way.&lt;/p&gt;

&lt;p&gt;The common answer is a separate user-mode networking helper. That is a good answer, and mature tools have earned their place. But while building Boxr, a rootless Open Container Initiative (OCI) engine written in Rust, I wanted to explore a second path: could the runtime carry a small, understandable networking path inside its own binary?&lt;/p&gt;

&lt;p&gt;That experiment became UserNet, an embedded Layer 2 through Layer 4 data path written in Rust. It is not a claim that a young implementation can replace every mature networking tool. It is a working fallback, a learning surface, and one of the most interesting pieces of Boxr to review.&lt;/p&gt;

&lt;p&gt;Rootless containers still need a way out&lt;/p&gt;

&lt;p&gt;Inside a private network namespace, a process can have its own interfaces, routes, and loopback device. That isolation is valuable, but it also cuts the process off from the host network.&lt;/p&gt;

&lt;p&gt;A rootful runtime can create bridges, virtual Ethernet pairs, routing rules, and network address translation with host-level capabilities. A rootless runtime has to work within a tighter boundary. It needs to translate traffic without assuming it can reconfigure the host.&lt;/p&gt;

&lt;p&gt;Boxr exposes several network modes because there is no single correct answer for every machine:&lt;/p&gt;

&lt;p&gt;Auto uses pasta when it is installed, then falls back to UserNet.&lt;/p&gt;

&lt;p&gt;UserNet selects the embedded Rust path explicitly.&lt;/p&gt;

&lt;p&gt;Pasta uses the external rootless networking driver.&lt;/p&gt;

&lt;p&gt;Bridge, host, and none support their expected isolation and connectivity models.&lt;/p&gt;

&lt;p&gt;That ordering is deliberate. Shipping an embedded fallback makes a minimal installation useful, while keeping pasta available recognizes the value of a mature implementation. Engineering is usually better when “built here” does not become “must be used everywhere.”&lt;/p&gt;

&lt;p&gt;A packet crosses five explicit stages&lt;/p&gt;

&lt;p&gt;UserNet begins with a TAP interface inside the container’s network namespace. From the application’s point of view, it is using normal POSIX sockets and a normal Linux network stack. The Linux stack emits Ethernet frames through the TAP device, and Boxr reads those frames in user space.&lt;/p&gt;

&lt;p&gt;The outbound path is:&lt;/p&gt;

&lt;p&gt;Container application  -&amp;gt; POSIX socket  -&amp;gt; Linux network stack in the container namespace  -&amp;gt; TAP interface (eth0)  -&amp;gt; Boxr UserNet  -&amp;gt; ordinary host UDP or TCP socket  -&amp;gt; destination&lt;/p&gt;

&lt;p&gt;The return path runs in reverse. Boxr receives bytes from a host socket, builds the protocol headers the container expects, computes checksums, wraps the packet in an Ethernet frame, and writes it back to TAP.&lt;/p&gt;

&lt;p&gt;The implementation keeps the layers visible rather than hiding them behind a large abstraction. Ethernet parsing chooses between Address Resolution Protocol (ARP) and IPv4. IPv4 parsing then dispatches Internet Control Message Protocol (ICMP), User Datagram Protocol (UDP), or Transmission Control Protocol (TCP). That directness makes the code approachable, but it also makes every protocol assumption visible—and reviewable.&lt;/p&gt;

&lt;p&gt;The small protocols are where trust begins&lt;/p&gt;

&lt;p&gt;Before TCP can work, the container has to believe there is a network on the other side of its interface.&lt;/p&gt;

&lt;p&gt;UserNet gives the container a small virtual network: the documented defaults use 10.0.2.15 for the container, 10.0.2.2 for the gateway, and 10.0.2.3 for DNS. When the container asks who owns the gateway or DNS address, the ARP handler returns a virtual gateway Media Access Control (MAC) address.&lt;/p&gt;

&lt;p&gt;IPv4 handling validates the header shape and calculates the standard one’s-complement checksum for generated packets. ICMP echo requests to the virtual gateway receive matching echo replies. These pieces are small, but they matter: if ARP, lengths, byte order, or checksums are wrong, the higher layers fail in ways that are difficult to diagnose from inside the container.&lt;/p&gt;

&lt;p&gt;DNS adds a more practical bridge. UserNet intercepts UDP queries sent to the virtual DNS address, forwards them through a host UDP socket to a resolver, then packages the response as UDP over IPv4 over Ethernet for the container. The application sees a DNS server. The host sees an ordinary UDP client. Boxr owns the translation between them.&lt;/p&gt;

&lt;p&gt;This is also where defensive parsing becomes essential. The input is a byte slice, not a trusted Rust object. Every header needs length checks before fields are read. Declared payload lengths have to be bounded by the bytes actually received. Checksums must be generated over exactly the right pseudo-header and payload. Rust removes broad classes of memory errors, but it does not make protocol logic correct by itself.&lt;/p&gt;

&lt;p&gt;TCP is the part that keeps the project honest&lt;/p&gt;

&lt;p&gt;ARP, ICMP, and a DNS proxy are finite enough to explain in a few paragraphs. TCP is where the state space expands.&lt;/p&gt;

&lt;p&gt;The current UserNet path recognizes connection setup and teardown flags, tracks sequence and acknowledgment numbers, opens unprivileged host TCP sockets for outbound destinations, and translates response data back into packets for the container. That is enough to demonstrate the architecture and exercise real traffic paths.&lt;/p&gt;

&lt;p&gt;It is not the same as claiming a production-grade, general-purpose TCP implementation.&lt;/p&gt;

&lt;p&gt;Real TCP behavior includes retransmission, duplicate acknowledgments, out-of-order segments, window scaling, backpressure, half-closed connections, resets, timing behavior, long-lived streams, and resource cleanup under failure. A happy-path handshake is the beginning of the work, not the end.&lt;/p&gt;

&lt;p&gt;That is why Boxr is labeled beta. The most valuable review is not “nice project.” It is a packet trace that shows the state machine made the wrong choice, a reproducible teardown failure, a malformed frame that escapes a boundary check, or a workload that exposes blocking behavior.&lt;/p&gt;

&lt;p&gt;I would rather make the boundary explicit than hide it behind the phrase “TCP/IP stack.” UserNet is useful today as an embedded path and a testable architecture. It still needs adversarial protocol review and sustained real-world use before anyone should treat it as mature network infrastructure.&lt;/p&gt;

&lt;p&gt;In-process networking trades isolation for simplicity&lt;/p&gt;

&lt;p&gt;Moving the networking path into the Boxr process changes the shape of the system.&lt;/p&gt;

&lt;p&gt;The benefits are tangible:&lt;/p&gt;

&lt;p&gt;One implementation language. Packet parsing, container lifecycle, and the surrounding runtime are all Rust.&lt;/p&gt;

&lt;p&gt;A smaller installation surface. UserNet does not require an additional networking binary to exist on the machine.&lt;/p&gt;

&lt;p&gt;A direct lifecycle. The engine owns the TAP loop and can stop it with the container.&lt;/p&gt;

&lt;p&gt;An inspectable data path. A contributor can follow a frame from Ethernet parsing to a host socket without crossing a foreign-function boundary.&lt;/p&gt;

&lt;p&gt;But “in process” is not automatically safer. A separate helper creates another component and another lifecycle to manage, yet it also creates a process boundary. With an embedded stack, packet parsing and the container engine share a fault domain. A panic, logic flaw, or resource-exhaustion bug can affect more of the runtime.&lt;/p&gt;

&lt;p&gt;So the security question is not “Rust or C?” It is: what inputs cross the boundary, what privileges does each component hold, what happens on failure, and which design can be tested and maintained well enough for the intended threat model?&lt;/p&gt;

&lt;p&gt;Boxr’s auto mode reflects that trade-off. It uses pasta when available and keeps UserNet as a zero-extra-dependency fallback. The goal is not purity. The goal is a rootless container that can reach the network while the architecture remains honest about its maturity.&lt;/p&gt;

&lt;p&gt;The fastest path forward is to break it usefully&lt;/p&gt;

&lt;p&gt;If you want to inspect the idea, the repository documents a direct UserNet path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kchaitanya863/boxr
&lt;span class="nb"&gt;cd &lt;/span&gt;boxr
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
./target/release/boxr run &lt;span class="nt"&gt;--network&lt;/span&gt; usernet &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rootless networking depends on the host environment, including Linux user-namespace support and access to /dev/net/tun, so the most useful reports include the kernel, distribution, architecture, exact command, logs, and a minimal reproducer. A short packet capture is even better when the failure is in the data path.&lt;/p&gt;

&lt;p&gt;The project recently shipped v0.1.44, which included Linux build and exec user-namespace fixes alongside refreshed launch assets. Boxr remains a young, single-maintainer beta, and that is precisely why focused contributors can have disproportionate impact now.&lt;/p&gt;

&lt;p&gt;Three areas would benefit most from experienced eyes:&lt;/p&gt;

&lt;p&gt;TCP behavior under unhappy paths. Retransmission, teardown, long-lived connections, and sequence handling deserve adversarial tests.&lt;/p&gt;

&lt;p&gt;Isolation boundaries. Is an in-process stack the right fault domain, or should the design preserve the same Rust implementation behind a process boundary?&lt;/p&gt;

&lt;p&gt;Packet-loop architecture. The current path is easy to understand; the next challenge is concurrency and throughput without making correctness impossible to reason about.&lt;/p&gt;

&lt;p&gt;If those problems sound interesting, read the networking architecture, join the design discussion, or open an issue with a workload that fails. If Boxr runs on your setup, report that too—the boring compatibility matrix is what turns an experiment into infrastructure.&lt;/p&gt;

&lt;p&gt;And if you want to follow a Rust container engine while the core design is still moving, star Boxr on GitHub. The next useful contribution might be a protocol test, a packet trace, or one carefully argued “this boundary should be somewhere else.”&lt;/p&gt;

</description>
      <category>rust</category>
      <category>containers</category>
      <category>docker</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
