176 lines
17 KiB
Markdown
176 lines
17 KiB
Markdown
# doos compared with Docker, Podman and others
|
|
|
|
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
|
|
|
This page puts doos next to the tools people usually reach for instead. It separates three kinds of claims, and every row says which one it is:
|
|
|
|
- **[M] measured** on the development machine, described in "Measurements" below.
|
|
- **[S] sourced** from the linked documentation or articles. Vendor blogs are marked as such and their numbers are not used.
|
|
- **[D] design**: what the doos source and [SPEC](SPEC.md) do today.
|
|
- **[K] general knowledge** that this research did not re-verify.
|
|
|
|
Nothing here is a roadmap. Where doos lacks something, the gap is listed with the code that would have to change.
|
|
|
|
Versions compared: doos 1.1.1, Docker 29.1.3 (rootful daemon on this machine), Podman 5.7.0 (netavark, crun, overlay), bubblewrap 0.11.1, on Ubuntu 26.04, kernel 7.0.0-38, 12 cores, one machine.
|
|
|
|
## 1. At a glance
|
|
|
|
| | doos | Docker (rootful) | Docker rootless | Podman rootless |
|
|
|---|---|---|---|---|
|
|
| Needs a daemon | no [D] | yes, root daemon [S: [Docker security](https://docs.docker.com/engine/security/)] | yes, per-user daemon [K] | no [S: [rootlesscontaine.rs](https://rootlesscontaine.rs/getting-started/podman/)] |
|
|
| Runs without root | yes [D] | no [S] | yes [S] | yes [S] |
|
|
| Who can use it safely | any user | only trusted users: access to the daemon is root-equivalent [S: [Docker security](https://docs.docker.com/engine/security/), [iximiuz](https://labs.iximiuz.com/tutorials/docker-security-introduction-a859718d)] | any user | any user |
|
|
| Image source | tarball, https URL with sha256, small catalog (Alpine, Ubuntu, Debian) [D] | OCI registries, Dockerfile builds [K] | same as Docker | OCI registries, Containerfile builds [K] |
|
|
| Build images | no [D] | yes (BuildKit) [K] | yes | yes (Buildah) [K] |
|
|
| Host dirs into the container (`-v`) | **no** [D] | yes [K] | yes | yes |
|
|
| Published ports | none for `slirp`; automatic for `pasta` [D] | `-p` [K] | `-p` | `-p` [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)] |
|
|
| Rootless network backend | slirp4netns or pasta [D] | n/a | RootlessKit based [S: [Podman blog](https://blog.podman.io/2023/07/podman-performance-root-and-rootless/)] | pasta default since 5.0 [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)] |
|
|
| Resource limits | memory, pids, cpus, written data [D][M] | memory, cpu, pids, more [K] | needs cgroup v2 + systemd delegation [S: [Docker rootless tips](https://docs.docker.com/engine/security/rootless/tips/)] | needs cgroup v2 [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)] |
|
|
| Default syscall filter | denylist: 48 listed calls plus rules for `clone`, `unshare`, `clone3` [D] | allowlist profile [S: [Docker seccomp](https://docs.docker.com/engine/security/seccomp/)] | same | built-in profile [K] |
|
|
| Default capabilities | Docker's default set [D][M] | that set [S] | that set | a smaller set [S: [dev.to comparison](https://dev.to/mechcloud_academy/docker-vs-podman-an-in-depth-comparison-2025-2eia)] |
|
|
| Orchestration (Compose, Kubernetes) | none [D] | Compose, Swarm, Kubernetes via runtimes [K] | same | Compose, pods, `kube play`, systemd units [K] |
|
|
| Background containers, logs, `stop` | none: a container lives while its first `attach` command lives [D] | yes [K] | yes | yes |
|
|
| Binary size | 552 KiB, 6 shared libraries, 3,445 lines of C++ [M] | CLI 29.9 MiB, plus `dockerd` and `containerd` [M][K] | same | 41 MiB [M] |
|
|
| Platforms | Linux only, tested on x86_64 only [D] | Linux, macOS, Windows (via a VM) [K] | Linux | Linux; macOS and Windows via a VM [K] |
|
|
| Maturity | version 1.1, one test machine | very mature, huge ecosystem | mature | mature |
|
|
|
|
## 2. Other tools, one line each
|
|
|
|
| Tool | What it is | Versus doos |
|
|
|---|---|---|
|
|
| bubblewrap (`bwrap`) | Low-level unprivileged sandbox builder, the engine behind Flatpak [S: [bubblewrap README](https://cdn.jsdelivr.net/gh/containers/bubblewrap/README.md), [Julia Evans](https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/)] | Smaller and faster to start [M], but you assemble every mount by hand and it has no image handling, no persistent state and no cgroup limits [K]; it maps a single id unless you add a helper [K]. doos is the "batteries included" layer on the same kernel features. |
|
|
| systemd-nspawn | Boots a whole OS tree as a container [S: [forum overview](https://forums.puri.sm/t/linux-containers-comparison-quest-for-the-best/13344)] | Rootless use is limited to a single UID/GID inside [S]. Not installed here, so not measured. |
|
|
| LXC / Incus (LXD fork) | System containers: a full distro with init and several services, close to a light VM [S: [OneUptime](https://oneuptime.com/blog/post/2026-03-18-choose-between-podman-and-lxc-lxd/view)] | Better for long-lived pet systems and multi-service setups, with API, storage pools and profiles. Heavier to set up. `lxc` binaries exist here, Incus does not, neither was run. |
|
|
| Apptainer (Singularity) | Rootless, daemonless, single-file SIF images, built for HPC (MPI, InfiniBand, GPUs) [S: [Alliance Canada](https://docs.alliancecan.ca/wiki/Apptainer), [NYU HPC](https://services.rt.nyu.edu/docs/hpc/containers/intro/)] | Different audience. It also has `--fakeroot` and signed immutable images [S], which doos lacks. Not installed here. |
|
|
| Distrobox / Toolbx | Podman- or Docker-backed "your distro inside your terminal", home directory shared on purpose, not a sandbox [S: [Arch wiki](https://wiki.archlinux.org/title/Distrobox), [comparison](https://computingforgeeks.com/distrobox-vs-toolbox/)] | Opposite goal: doos hides the host's `/home` by default. |
|
|
| gVisor | A user-space kernel that answers most syscalls itself, cutting host-kernel exposure at an I/O cost [S: [Edera](https://edera.dev/stories/kata-vs-firecracker-vs-gvisor-isolation-compared), [Northflank](https://northflank.com/blog/kata-containers-vs-firecracker-vs-gvisor)] | Stronger isolation than doos, needs its own runtime. Vendor blogs; their numbers are not used here. |
|
|
| Kata Containers / Firecracker | Each workload in its own KVM micro-VM with its own kernel [S: same sources, [MASCOTS 2020 paper](https://cristian.kleinlabs.eu/publications/mascots2020_container_runtime_security.pdf)] | The right tool for hostile or multi-tenant code. doos shares the host kernel, so a kernel bug is a doos escape. |
|
|
|
|
## 3. Measurements
|
|
|
|
Method: trivial command (`true`) started 10 times in a row, average wall time per run, warm caches, the Alpine image already present for every tool, one machine, one run set. Scripts were ad-hoc shell loops and are not part of the repository tests, so treat the numbers as indicative, not as a benchmark.
|
|
|
|
| Setup | Time per run |
|
|
|---|---|
|
|
| bubblewrap, host `/usr`, all namespaces unshared, no network | 9 ms |
|
|
| bubblewrap, host `/usr`, network shared | 8 ms |
|
|
| doos host mode, `--net none` | 14 ms |
|
|
| doos image Alpine, `--net none` | 16 ms |
|
|
| doos host mode, slirp | 54 ms |
|
|
| doos image Alpine, slirp | 57 ms |
|
|
| doos image Alpine, pasta | 65 ms |
|
|
| Podman, Alpine, `--network none` | 214 ms |
|
|
| Podman, Alpine, default network | 234 ms |
|
|
| Docker (rootful), Alpine, `--network none` | 590 ms |
|
|
| Docker (rootful), Alpine, default bridge | 456 ms |
|
|
|
|
How to read this honestly:
|
|
|
|
- doos is fast partly because it does less: no image store database, no registry code, no layer unpacking per run, no container metadata service. The extra time over bubblewrap is the id map helpers (`newuidmap`, `newgidmap`), the overlay mounts, a hardening filter and the network helper process.
|
|
- Docker here is a rootful daemon, so its numbers include a client-to-daemon round trip. Rootless Docker was not measured.
|
|
- The Docker row "none" is slower than "default" in this run; the spread between runs was not measured, so do not read a ranking into it.
|
|
- Startup is one number among many. It matters for scripts that start hundreds of short-lived sandboxes. It does not matter for a service that starts once.
|
|
|
|
Disk, measured with `du` (which cannot read overlayfs' mode-000 work directories, so true usage is slightly higher):
|
|
|
|
| Item | Size |
|
|
|---|---|
|
|
| doos Alpine image, extracted once and shared | 8.3 MiB |
|
|
| doos Ubuntu base image, extracted once and shared | 98 MiB |
|
|
| one doos container on top of an image | a few KiB of metadata plus its own writes |
|
|
| four containers (one Alpine, three Ubuntu) on shared images | 20 KiB together |
|
|
| doos host-mode container after its first create (skeleton, `/etc` allowlist, dpkg state) | 4 MiB, created in 0.5 s |
|
|
| Podman storage on this machine (many unrelated images) | 26.3 GB images, 1.8 GB containers |
|
|
|
|
The Podman figure is this machine's accumulated state, not a like-for-like comparison. What the comparison does show is the model: doos extracts an image once and every container adds only its own writes.
|
|
|
|
## 4. doos versus Docker
|
|
|
|
**Where doos is better**
|
|
|
|
- No daemon and no root. With Docker's default install, anyone who can talk to the daemon controls the machine [S]. doos has no such trust boundary: it can only do what the invoking user can do, plus what the `newuidmap` helper allows.
|
|
- Tiny and auditable: 552 KiB, 3,445 lines [M], one process tree, no background service, nothing listening.
|
|
- Instant: 14 to 65 ms against 456 to 590 ms for a rootful Docker run [M].
|
|
- Safe defaults a user does not have to remember: Docker's default capability set, a seccomp filter, an allowlist of host `/etc`, a memory and pid cap, a written-data cap [D][M].
|
|
- A "host mode" that reuses the host's installed tools (compilers, `apt` state) for throwaway shells with no image download [D].
|
|
- A size cap that freezes instead of filling the disk [D][M]. Docker's writable-layer size limit needs the overlay2 driver on XFS with project quotas [S: [Fabian Lee](https://fabianlee.org/2020/01/15/docker-use-overlay2-with-an-xfs-backing-filesystem-to-limit-rootfs-size/)].
|
|
|
|
**Where Docker is better**
|
|
|
|
- Everything around images: registries, `docker build`, layer caching, multi-arch, signing and scanning ecosystems [K]. doos cannot pull an arbitrary `nginx:1.27` from a registry.
|
|
- Host directories and devices into the container (`-v`, `--device`, GPUs). doos has no bind-mount option [D].
|
|
- Long-running services: background containers, restart policies, healthchecks, logs, `exec`, `stop` [K]. In doos a container exists only while its first `attach` command runs [D].
|
|
- Networking: user-defined networks, DNS between containers, published ports on any interface [K].
|
|
- Compose, Swarm and the Kubernetes ecosystem [K].
|
|
- macOS and Windows support through a VM [K].
|
|
- Strongest default syscall posture: an allowlist, which denies unknown new syscalls. The doos filter is a denylist, so a new kernel syscall is allowed until it is added [D][S].
|
|
|
|
## 5. doos versus Podman
|
|
|
|
Podman is the closest comparison, since both are daemonless and rootless.
|
|
|
|
**Where doos is better**
|
|
|
|
- Smaller, faster to start [M] and one binary with no storage database. Podman's rootless setup brings a store under `~/.local/share/containers` with its own metadata [K].
|
|
- Opinionated limits and hardening out of the box, with a stated, measured effect (OOM kill, pids, cpus, freeze at a size cap) [M].
|
|
- Two network backends with different, documented trade-offs, including a host-like mode that keeps host abstract unix sockets out of the container [M].
|
|
- Image verification is explicit: sha256 plus signatures checked against fingerprints pinned in the source [D][M].
|
|
- Frozen-at-the-cap behaviour with a never-frozen rescue shell [D][M].
|
|
|
|
**Where Podman is better**
|
|
|
|
- OCI compatibility end to end: pull, push, build with Buildah, pods, `kube play`, Quadlet and systemd integration [K]. It can also run images made for Docker unchanged.
|
|
- Docker-compatible CLI and socket, so existing tooling mostly works [K].
|
|
- Bind mounts, volumes, secrets, `exec`, `logs`, `stop`, restart policies [K].
|
|
- Years of hardening, a security team, and a large user base finding the sharp edges. doos has one test machine.
|
|
- Podman documents rootless caveats: ports below 1024, `/etc/subuid` setup, cgroup v2 only, and a rootless port forwarder that does not preserve client source IPs [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)]. doos shares the subuid and cgroup v2 requirements [D]. Binding ports below 1024 in doos was not tested.
|
|
|
|
**What both share:** the same kernel features (user, mount, pid, network namespaces, overlayfs, cgroup v2, seccomp), the same pasta/slirp4netns options, and the same weakness: they share the host kernel [S].
|
|
|
|
## 6. Security, side by side
|
|
|
|
- **Shared kernel.** doos, Docker, Podman, bubblewrap, nspawn and LXC all run container processes on the host kernel. A kernel vulnerability reachable through allowed syscalls is a host vulnerability. gVisor reduces that exposure at a performance cost; Kata and Firecracker remove it by giving each workload its own kernel [S].
|
|
- **Rootless is the real difference from rootful Docker.** A bug that gives root inside a rootless doos or Podman container yields the invoking user's rights on the host, not root [D][S].
|
|
- **doos filter style.** A denylist built at runtime with an architecture check, x32 guard and `clone3` handling [D]. Docker and Podman ship allowlist profiles [S]. An allowlist is the more conservative design.
|
|
- **Missing layers.** Docker applies an AppArmor profile (`docker-default`) on this machine [M: `docker info` lists apparmor]. doos installs no AppArmor or SELinux profile of its own; its only profile exists to *permit* user namespaces on Ubuntu 24.04+ [D].
|
|
- **Supply chain.** doos pins publisher signing keys for Alpine and Ubuntu and checks digests for Debian [D][M]. Registry-based tools rely on registry trust, content trust or external signing tools [K].
|
|
|
|
Full list of what doos does not cover is at the end of [RESEARCH](docs/RESEARCH.md); the exact hardening is in [SPEC](SPEC.md) under Hardening.
|
|
|
|
## 7. What doos lacks, and which code would change
|
|
|
|
These are the gaps that matter when comparing, each next to where it lives. They are descriptions, not promises.
|
|
|
|
| Gap | Where |
|
|
|---|---|
|
|
| No OCI registry pull, no layers, no build | `src/image.cpp` (`Catalog`, `ImageStore`) |
|
|
| No host directory bind mounts, no devices, no GPU | `setupRoot` in `src/sandbox.cpp`, option parsing in `src/cli.cpp` |
|
|
| No background containers, `stop`, `exec` by name, logs | lifetime model in `Sandbox::start` / `Sandbox::watch` (container lives with its first command) |
|
|
| No user-defined networks, no `-p` | `src/network.cpp` |
|
|
| No Compose or Kubernetes integration | not present |
|
|
| Environment, user, working-directory flags missing | `applyLimitsAndEnv` in `src/sandbox.cpp` |
|
|
| Linux x86_64 tested only | `src/hardening.cpp` guards aarch64, never run there |
|
|
| seccomp is a denylist | `deniedSyscalls` in `src/hardening.cpp` |
|
|
| Test coverage: one machine | `tests/run.sh` |
|
|
|
|
## 8. Conclusion
|
|
|
|
doos is not a Docker replacement and does not try to be. It is a small, rootless, daemonless tool for **disposable or per-user Linux shells and sandboxes that start in tens of milliseconds, with safe defaults and hard caps already set**. Its strengths are the ones the measurements show: it is a 552 KiB single binary, it starts in 14 to 65 ms [M], it runs with no daemon and no root, it caps memory, processes, CPU and written data by default, and it verifies what it downloads.
|
|
|
|
Choose by the job:
|
|
|
|
| If you need | Use |
|
|
|---|---|
|
|
| A quick, isolated shell or script runner for one user, host tools or a distro image, with limits, no daemon | **doos** |
|
|
| OCI images, Dockerfiles or Containerfiles, registries, CI that matches production | **Podman** (rootless, daemonless) or **Docker** |
|
|
| Compose, Kubernetes parity, Docker ecosystem tooling | **Docker** or **Podman** with its Docker compatibility |
|
|
| Sandbox one application with exact control over mounts | **bubblewrap** |
|
|
| A full OS with init and several services, long lived | **Incus / LXC** or **systemd-nspawn** |
|
|
| Shared HPC cluster, MPI, GPUs, signed single-file images | **Apptainer** |
|
|
| A desktop environment tied to your home directory | **Distrobox / Toolbx** |
|
|
| Running code you do not trust at all, or multi-tenant workloads | **Kata / Firecracker** (or gVisor as a middle step), not any shared-kernel container |
|
|
|
|
Honest limits of this comparison: one machine, 10-run averages, no rootless Docker, no systemd-nspawn, Incus or Apptainer measurements, and several Docker and Podman capabilities listed as general knowledge rather than re-verified. The conclusion about when to use doos rests on what the doos tests and measurements show ([RESEARCH](docs/RESEARCH.md)), not on a claim that it beats mature tools on features. On features it does not.
|
|
|
|
Back to [README](README.md) for usage, [SPEC](SPEC.md) for the exact behavior, [ARCHITECTURE](docs/ARCHITECTURE.md) for how it works, [AGENTS](AGENTS.md) for the rules, and [CHANGELOG](CHANGELOG.md) for history.
|