Why Bubblewrap Is Worth a Look for Fast Containers
Container startup time becomes a real problem when you’re starting a new container on every HTTP request. Docker and Podman both take roughly 300ms to run even a trivial command like ls — most of that time is tooling overhead. Running plain ls takes about 3ms.
That overhead doesn't matter for long-running workloads like webservers, but if you're building something like a playground where each request spins up an isolated process, 300ms is painful. The good news: you can get container startup under 10ms with bubblewrap, a lower-level tool that gives you Linux namespaces directly without the Docker/Podman machinery.
What Bubblewrap Does Differently
Bubblewrap is not an OCI runtime. It doesn't manage container images, overlay filesystems, or network bridges. Instead, it maps host directories into a new namespace — think of it as a sandboxing tool more than a container runtime (it actually comes from the Flatpak project).
Here's what a minimal invocation looks like — a container with host root read-only and /tmp writable:
bwrap \
--ro-bind / / \
--bind /tmp /tmp \
--proc /proc --dev /dev \
--unshare-pid \
--unshare-net \
bash
The speed result speaks for itself at 8ms for ls:
$ time bwrap --ro-bind / / --proc /proc --dev /dev --unshare-pid ls /
Executed in 8.04 millis
Running as an Unprivileged User
Docker runs containers as root via a daemon, even when you invoke it as a regular user. Podman is different: it runs rootless containers using user namespaces, so your normal user starts the container and appears as root inside it.
$ podman run -it ubuntu:20.04 ls
Bubblewrap works the same way. It runs containers as a non-root user using user namespaces, which is important if you're building something without the protection of a hardened daemon.
Running a Container Image
You can use bubblewrap with an existing Docker image, though you'll need to unpack it yourself. There's no image pulling built in.
mkdir rootfs
docker export $(docker create frapsoft/fish) | tar -C rootfs -xf -
bwrap \
--bind $PWD/rootfs / \
--proc /proc --dev /dev \
--uid 0 \
--unshare-pid \
--unshare-net \
fish
One caveat: bubblewrap doesn't create an overlay filesystem for writes, so the container can modify the image directory in place. If that matters, you'll need to manage that yourself.
Three Weird Behaviors to Know About
Because bubblewrap is lower-level, you can hit states that wouldn't happen with Docker or Podman. These three came up in practice.
1. Processes in ps That Don't Exist
ps doesn't list processes from your PID namespace — it lists entries in /proc. If you mount the host's /proc into the container, you'll see host processes you can't actually interact with.
$ bwrap --ro-bind / / --unshare-all bash
$ ps aux
... some processes
root 390073 0.0 0.0 2848 124 pts/9 S 14:28 0:00 bwrap --ro-bind / / --unshare-all --uid 0 bash
... some other processes
$ kill 390073
bash: kill: (390073) - No such process
$ ps aux | grep 390073
root 390073 0.0 0.0 2848 124 pts/9 S 14:28 0:00 bwrap --ro-bind / / --unshare-all --uid 0 bash
Kill attempts just fail with no such process. Fixing it is straightforward — mount the container's own proc filesystem:
$ bwrap --ro-bind / / --unshare-all --dev /dev --proc /proc ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
bork 1 0.0 0.0 3644 136 ? S+ 16:21 0:00 bwrap --ro-bind / / --unshare-all --dev /dev --proc /proc ps au
bork 2 0.0 0.0 21324 1552 ? R+ 16:21 0:00 ps aux
That lists only the two processes actually running in your namespace.
2. Root Isn't All-Powerful
Passing --uid 0 makes you root inside the container, but that root doesn't have the Linux capabilities needed for privileged operations. For instance, listening on port 80 fails because you need CAP_NET_BIND_SERVICE:
$ bwrap --ro-bind / / --unshare-all --uid 0 nc -l 80
nc: Permission denied
You can add the capability explicitly:
$ bwrap --ro-bind / / --unshare-all --uid 0 --cap-add cap_net_bind_service nc -l 80
(no output, success!!!)
This works, though figuring out which capabilities you need can be an iterative process: run, hit an error, consult man capabilities, add a --cap-add, repeat.
Oddly, adding --dev /dev to the working command breaks it again. Whether that's a bubblewrap bug or something else is undetermined.
$ bwrap --ro-bind / / --dev /dev --unshare-all --uid 0 --cap-add cap_net_bind_service nc -l 80
nc: Permission denied
3. UID Mapping Breaks Tools That Use Other Users
Running apt-get update inside an Ubuntu container fails badly:
mkdir rootfs
docker export $(docker create ubuntu:20.04) | tar -C rootfs -xf -
bwrap \
--bind $PWD/rootfs / \
--proc /proc\
--uid 0 \
--unshare-pid \
apt-get update
The error messages are misleading — adding capabilities doesn't help:
E: setgroups 65534 failed - setgroups (1: Operation not permitted)
E: setegid 65534 failed - setegid (22: Invalid argument)
E: seteuid 100 failed - seteuid (22: Invalid argument)
E: setgroups 0 failed - setgroups (1: Operation not permitted)
.... lots more similar errors
The real issue is in the UID map. Bubblewrap as configured only maps user 0 (which maps to your host user) and user 1000:
root@kiwi:/# cat /proc/self/uid_map
0 1000 1
root@kiwi:/# cat /proc/self/gid_map
1000 1000 1
According to man user_namespaces, only mapped values can be used in system calls that change user IDs. apt tries to use users 100 and 65534, neither of which are in the map, so those operations fail.
Podman avoids this by mapping a whole range of users:
$ podman run -it ubuntu:20.04 bash
root@793d03a4d773:/# cat /proc/self/uid_map
0 1000 1
1 100000 65536
root@793d03a4d773:/# cat /proc/self/gid_map
0 1000 1
1 100000 65536
There are workarounds for bubblewrap, but they're nontrivial. If you hit this, know that the default UID mapping is the culprit.
Not a Firecracker Replacement
If you're considering Firecracker for this use case, note that VMs use far more memory (around 50MB at rest) and showed startup times of 2–3 seconds on a small machine. There's a whole operating system to boot. A process-level sandbox like bubblewrap is simply a different order of magnitude.
Bubblewrap Works Great for Constrained Tasks
Despite the caveats, bubblewrap is effective for scenarios where you need to run a single well-defined command in an isolated namespace — like running git against a mapped repository. That kind of task is simple to set up, has none of the weird issues above, and runs at near-native speed. For projects where every millisecond of startup counts, it's a very compelling alternative to the heavier container tools.



