Add --mount/--davmount WebDAV auto-mount via a managed, verified rclone
ci / test (push) Waiting to run
ci / audit (push) Waiting to run
ci / build (push) Successful in 1m25s
ci / valgrind (push) Successful in 2m37s

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 08:05:02 +01:00
co-authored by Claude Sonnet 5
parent 7adf43dd03
commit d1541b77e6
25 changed files with 1096 additions and 220 deletions
+37 -4
View File
@@ -9,15 +9,16 @@ How doos works internally. The behavior contract is [SPEC](../SPEC.md); this exp
| File | Class / role |
|---|---|
| `src/spec.hpp` | `spec::` constants: caps, mount lists, device list, paths. Single source of truth with `SPEC.md`. |
| `src/util.{hpp,cpp}` | `SysError`, `Fd` (RAII fd), `FileLock` (bounded flock), `parseSize`/`formatSize`, `base64Decode`, `runProcess` (fork/exec, capped capture), `fsutil` (mkdirs, read/write, bounded `removeTree`, `mirrorDirs` skeleton, `copyTree`/`copyEntries` (with skip predicate), `copyFiles`, `diskUsage` with hardlink dedupe). |
| `src/util.{hpp,cpp}` | `SysError`, `Fd` (RAII fd), `FileLock` (bounded flock), `parseSize`/`formatSize`, `base64Decode`, `runProcess` (fork/exec, capped capture), `fetchText`/`fetchFile` (curl, https only, capped), `TempDir` (scratch dir under the state dir, removed on scope exit), `isHex64`, `verifyDetachedSignature`/`verifyClearsignedSignature` (gpg with a throwaway home, pinned fingerprint; shared by `image.cpp` and `davmount.cpp`), `fsutil` (mkdirs, read/write, bounded `removeTree`, `mirrorDirs` skeleton, `copyTree`/`copyEntries` (with skip predicate), `copyFiles`, `diskUsage` with hardlink dedupe). |
| `src/idmap.{hpp,cpp}` | `IdMap` (single or subuid mapping via newuidmap/newgidmap, subuid range lookup), `UserNsJob` (run a function as root of a user namespace with the same map: image extraction, deleting subuid-owned trees). |
| `src/image.{hpp,cpp}` | `Catalog` (alpine, ubuntu, debian resolution from publishers), `verifySignature` (gpg with a throwaway home, pinned fingerprint), `ImageStore` (add with download, sha256 and signature check, extraction; list; delete). |
| `src/image.{hpp,cpp}` | `Catalog` (alpine, ubuntu, debian resolution from publishers), `ImageStore` (add with download, sha256 and signature check via `util::verifyDetachedSignature`, extraction; list; delete). |
| `src/davmount.{hpp,cpp}` | `DavMount`: downloads/verifies/installs a pinned `rclone` under the state dir, resolves a `--mount`/`--davmount` WebDAV entry to a ready, reused, managed FUSE mount (on-the-fly connection string, double-fork daemonize, mountinfo + active-listing readiness, failure classification), `list`/`umount` for `doos davmount`. |
| `src/cgroup.{hpp,cpp}` | `Limits`, `Cgroup`: `doos/<name>/{main,shell}` under the user's delegated cgroup; limits on the parent, freeze on `main`, later attaches in `shell`, OOM and pid-limit counters. |
| `src/hardening.{hpp,cpp}` | `Hardening::apply`: capability bounding set to the Docker default, seccomp-bpf denylist built at runtime (arch check, x32 guard, clone3 ENOSYS, CLONE_NEWUSER filter). |
| `src/network.{hpp,cpp}` | `NetMode` (slirp, pasta, host, none) + `Network` (parse, resolv.conf per mode), `SlirpSession`, `PastaSession` (own the helper processes). |
| `src/container.{hpp,cpp}` | `Options` (what the user asked for), `Container` (paths, meta, pid file, layout, one-time seeding, usage), `Store` (list, get_or_create with on-demand image fetch, remove). |
| `src/sandbox.{hpp,cpp}` | `Sandbox`: start a container (clone + id map + mounts + pivot_root + init), watch it (size cap), or join a running one (setns). |
| `src/cli.{hpp,cpp}` | `Cli`: argument parsing, the four container commands and `image list/add/delete`. |
| `src/sandbox.{hpp,cpp}` | `Sandbox`: resolves davmounts (`DavMount::resolveAll`), starts a container (clone + id map + mounts + pivot_root + init), watches it (size cap), or joins a running one (setns). |
| `src/cli.{hpp,cpp}` | `Cli`: argument parsing, the four container commands, `image list/add/delete` and `davmount list/umount`. |
| `src/main.cpp` | Catches exceptions, prints `doos: <message>`, exit 1. |
| `tests/run.sh` | End-to-end suite. |
| `scripts/release.sh` | Bump, build, test, commit, tag. |
@@ -33,6 +34,7 @@ Cli::cmdAttach
runningPid()? yes -> join() no -> start()
start():
DavMount::resolveAll(mounts) before clone: webdav entries become ready, reused Bind entries
clone(NEWUSER|NEWNS|NEWPID|NEWUTS|NEWIPC[|NEWNET]) child = container pid 1
IdMap::apply(child, ids) single: write /proc maps; subuid: newuidmap + newgidmap
slirp mode: SlirpSession.start(child) waits for ready-fd
@@ -86,6 +88,37 @@ unmount of the old root.
Reasons are in `SPEC.md` (host root-owned files appear as `nobody`).
## davmount flow
```
DavMount::resolveAll(mounts) (Sandbox::start, before clone)
for each WebDav entry:
id = fnv1a(url, user, pass, vendor, insecure) dst excluded: shared across containers
dir = $DOOS_HOME/davmounts/<id>/, mkdirs mnt/, FileLock dir/lock
already in mountinfo (fuse.*) and pidfile's pid alive and an active listing succeeds?
-> reuse mnt/ as is (sub-second)
-> else unmountQuiet (fusermount3 -u, then -uz if still mounted) and fall through
ensureRclone(): $DOOS_HOME/tools/rclone-VERSION/rclone; `rclone version` match -> reuse,
else download+verify (util::fetchFile, util::verifyClearsignedSignature against the
pinned key, sha256 against the signed SHA256SUMS), unzip, atomic rename into place
rclone obscure <pass> (only if a password was given)
connection string ":webdav,url=\"...\",vendor=\"...\"[,user=...][,pass=...][,no_check_certificate=true]:"
spawnRcloneMount: pipe, fork, fork again, setsid + redirect to dir/log, execve(rclone, mount, ...)
first fork writes the second fork's pid down the pipe, then exits (reaped immediately);
the real daemon is now a child of init, outliving this doos process entirely
poll up to kDavMountReadyTimeoutMs: daemon pid alive? mountinfo shows it AND a real directory
listing succeeds (stat() alone answers without touching the backend; only listing does)?
-> ready, write dir/pid + dir/meta (never the password)
-> daemon died or never answers in time: unmountQuiet, classify dir/log's tail
(401/Unauthorized, x509/certificate, 501/Unsupported method, dial/host/refused/timeout,
else "see the log"), throw
Bind entries pass through unchanged; every resolved entry becomes a plain Bind MountSpec
pointing at dir/mnt, so mountExtra() (sandbox.cpp) never has to know davmount exists.
```
`doos davmount list`/`umount` walk/remove `$DOOS_HOME/davmounts/*` directly; `umount` is dry-run unless
`--yes`, same pattern as `delete`/`image delete`.
## Images
`ImageStore::add`: lock-free staging dir `images/.tmp-NAME`, download with curl (https only, size and time caps),
+11 -2
View File
@@ -66,7 +66,16 @@ removed afterwards. Rows marked "unmeasured" have no run behind them.
| G9 | `parseSize` left-shifted without an overflow check: `--size 16777217t` wrapped to exactly `1.0T` and was accepted instead of rejected | check `v > (UINT64_MAX >> shift)` before shifting, reject instead of wrapping | **done in 1.3.2**, reproduced before the fix, re-run after: rejected | `parseSize` in `src/util.cpp` |
| G10 | `newuidmap`/`newgidmap`/`slirp4netns`/`pasta` were pinned to a single hardcoded `/usr/bin/...` path, unlike `curl`/`tar`/`sha256sum`/`gpg` which go through `resolveBinary()`'s state-dir-override-then-fixed-dirs search; a distro installing them elsewhere would fail even though PATH-poisoning was never possible either way | route all four through `resolveBinary()`, same as the other helpers | **done in 1.3.2** | `idmap.cpp`, `network.cpp` |
| G11 | Alpine's `latest-releases.yaml` and Debian's `index.json` (embedded base64 manifest) are read with fixed string/byte-offset search, not a YAML/JSON library; a publisher reformatting either file breaks extraction with a thrown error (fails closed) rather than silently misreading it | none: a dependency to parse two small, publisher-controlled files is a worse trade than failing loudly when their format changes, given the project's own "fewer dependencies" rule | **accepted, not fixed**; this row exists so the trade-off is written down, not rediscovered | `Catalog::resolve` in `src/image.cpp` |
| G12 | No way to give a container storage that does not count against `--size` (remote/network storage, e.g. a WebDAV share); every writable path was either the host overlay (capped) or a private dir (capped) | `--mount SRC:DST[:ro]`: bind-mount an already-mounted host path into the container, outside the size watchdog's scan set (`Container::usedBytes` only scans `layers/`/`private/`, never `--mount` targets). doos itself never speaks a network filesystem protocol: mount WebDAV/NFS/etc. on the host first (e.g. `rclone mount`), doos only bind-mounts what is already there | **done in 1.5.0** (`--mount`, max 8 per container, changeable, applies at next start); missing `SRC` fails the start loudly | `Container::mounts`, `mountExtra` in `src/sandbox.cpp` |
| G12 | No way to give a container storage that does not count against `--size` (remote/network storage, e.g. a WebDAV share); every writable path was either the host overlay (capped) or a private dir (capped) | `--mount SRC:DST[:ro]`: bind-mount an already-mounted host path into the container, outside the size watchdog's scan set (`Container::usedBytes` only scans `layers/`/`private/`, never `--mount` targets). doos itself never speaks a network filesystem protocol: a plain `--mount` bind needs the host path mounted already; see G13 for WebDAV specifically | **done in 1.5.0** (`--mount`, max 8 per container, changeable, applies at next start); missing `SRC` fails the start loudly | `Container::mounts`, `mountExtra` in `src/sandbox.cpp` |
| G13 | G12's `--mount` still needed the user to mount WebDAV themselves (e.g. `rclone mount`) before pointing `--mount` at the result; no in-box path to remote storage | `--mount`/`--davmount` with a `dav://`, `davs://`, `http://` or `https://` source: doos downloads, GPG-verifies and installs a pinned `rclone` build under `$DOOS_HOME/tools/` (never the system, never PATH), then drives it on the host (on-the-fly connection string, no config file; detached double-fork daemon so the mount outlives the `attach` that created it) and bind-mounts the result exactly like a G12 bind. Deduplicated and reused by target (url+creds+vendor), not by destination | **done**, measured below under "davmount" | `src/davmount.{hpp,cpp}`, `DavMount::resolveAll` called from `Sandbox::start` |
### davmount (G13, measured against a local `rclone serve webdav` fixture)
- First use (fresh `$DOOS_HOME`, no managed rclone yet): `rclone-v1.75.2-linux-amd64.zip` (~20 MiB) downloaded, `SHA256SUMS` GPG-verified against the pinned fingerprint (`gpg` here accepts the DSA/SHA-1 signature rclone's release process still uses; it is a key-import plus `GOODSIG`/`VALIDSIG` fingerprint match, the same check as Ubuntu's image signature, not the web-of-trust warning gpg also prints), sha256 matched, unzipped, installed; the whole install plus the first mount completed in a few seconds and only ever happens once per `$DOOS_HOME`.
- Reuse (second, unrelated container against the same target): measured at 41 ms and 45 ms for `attach -- true`, i.e. no remount cost at all; the existing FUSE mount is confirmed healthy (pid alive, mountinfo entry present, a real directory listing succeeds) and reused as is.
- Write durability: `--vfs-cache-mode=writes` (rclone's own recommendation for WebDAV, which cannot stream) queues a write for upload after `--vfs-write-back` (shortened here to 1 s from rclone's 5 s default). Measured: a file written from inside a container appeared on the backend about 5 s after the *default* write-back elapsed in one run; with `--vfs-write-back=1s` the same write reached the backend within about 1-2 s of the writing process exiting, confirmed independent of the container's own lifetime (the container had already exited; the detached rclone daemon kept running and finished the upload on its own).
- Failure classification, each reproduced against the same local fixture: wrong `--dav-pass` surfaced the server's `401 Unauthorized` and was reported as "authentication failed"; an unresolvable/unreachable address and a non-WebDAV endpoint (plain HTTP server, `501 Unsupported method ('PROPFIND')`) were reproduced in manual testing with the matching classifications. Every case failed within a few seconds, never the full 20 s readiness budget, because rclone mounts eagerly and only a real directory listing (not `stat` on the FUSE root, which always answers without touching the backend) forces the round trip that surfaces the error.
- `doos davmount list`/`umount --yes`: verified showing a live target's id/vendor/url and state, dry-run output naming the path and warning it stops storage for any container still using it, and `--yes` actually unmounting (`fusermount3 -u`) and removing the managed directory.
### G1: memory, cpu and pids limits (cgroup v2)
@@ -272,7 +281,7 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
![Horizontal bar chart of CPU-seconds used by four busy loops in one container, without a limit and with --cpus 0.5. The limited container is highlighted. Values are in the sentence below.](charts/cpu.svg)
The chart above is generated from `docs/charts/bench.json` (`make bench`, then `make charts`); it shows <!--bench:cpu_free-->15.6<!--/bench--> CPU-seconds without a limit and <!--bench:cpu_half-->2.4<!--/bench--> with `--cpus 0.5`; the startup and size charts live in [COMPARISON](../COMPARISON.md).
The chart above is generated from `docs/charts/bench.json` (`make bench`, then `make charts`); it shows <!--bench:cpu_free-->16.3<!--/bench--> CPU-seconds without a limit and <!--bench:cpu_half-->2.3<!--/bench--> with `--cpus 0.5`; the startup and size charts live in [COMPARISON](../COMPARISON.md).
### What changed in behavior (and why it is not a loss)
+6 -6
View File
@@ -1,17 +1,17 @@
{
"meta": {
"date": "2026-10-24",
"date": "2026-10-25",
"kernel": "7.0.0-38-generic",
"cpus": 12,
"runs": 10,
"doos": "1.4.0",
"doos": "1.5.0",
"podman": "podman version 5.7.0",
"docker": "Docker version 29.1.3, build 29.1.3-0ubuntu4.1",
"bubblewrap": "bubblewrap 0.11.1",
"src_lines": 3712,
"src_lines": 4413,
"shared_libs": 6
},
"startup": [{"id":"bwrap_none","label":"bubblewrap (host /usr)","group":"none","kind":"other","ms":8.8},{"id":"bwrap_net","label":"bubblewrap (host /usr)","group":"default","kind":"other","ms":7.8},{"id":"doos_host_none","label":"doos host mode","group":"none","kind":"doos","ms":20.2},{"id":"doos_alpine_none","label":"doos Alpine image","group":"none","kind":"doos","ms":14.2},{"id":"doos_host_slirp","label":"doos host mode","group":"default","kind":"doos","ms":61.4},{"id":"doos_alpine_slirp","label":"doos Alpine image","group":"default","kind":"doos","ms":58.0},{"id":"doos_alpine_pasta","label":"doos Alpine, pasta","group":"default","kind":"doos","ms":82.5},{"id":"podman_none","label":"Podman Alpine","group":"none","kind":"other","ms":282.9},{"id":"podman_net","label":"Podman Alpine","group":"default","kind":"other","ms":313.2},{"id":"docker_none","label":"Docker Alpine (rootful)","group":"none","kind":"other","ms":462.6},{"id":"docker_net","label":"Docker Alpine (rootful)","group":"default","kind":"other","ms":645.1}],
"sizes": [{"id":"bwrap","bytes":80424},{"id":"crun","bytes":483392},{"id":"doos","bytes":544640},{"id":"docker","bytes":31374336},{"id":"podman","bytes":43180896}],
"cpu": {"wall_seconds": 4.2, "busy_loops": 4, "unlimited": 15.56, "half_cpu": 2.38}
"startup": [{"id":"bwrap_none","label":"bubblewrap (host /usr)","group":"none","kind":"other","ms":9.2},{"id":"bwrap_net","label":"bubblewrap (host /usr)","group":"default","kind":"other","ms":8.0},{"id":"doos_host_none","label":"doos host mode","group":"none","kind":"doos","ms":16.2},{"id":"doos_alpine_none","label":"doos Alpine image","group":"none","kind":"doos","ms":14.7},{"id":"doos_host_slirp","label":"doos host mode","group":"default","kind":"doos","ms":60.0},{"id":"doos_alpine_slirp","label":"doos Alpine image","group":"default","kind":"doos","ms":56.2},{"id":"doos_alpine_pasta","label":"doos Alpine, pasta","group":"default","kind":"doos","ms":66.0},{"id":"podman_none","label":"Podman Alpine","group":"none","kind":"other","ms":216.0},{"id":"podman_net","label":"Podman Alpine","group":"default","kind":"other","ms":232.9},{"id":"docker_none","label":"Docker Alpine (rootful)","group":"none","kind":"other","ms":345.8},{"id":"docker_net","label":"Docker Alpine (rootful)","group":"default","kind":"other","ms":449.1}],
"sizes": [{"id":"bwrap","bytes":80424},{"id":"crun","bytes":483392},{"id":"doos","bytes":704512},{"id":"docker","bytes":31374336},{"id":"podman","bytes":43180896}],
"cpu": {"wall_seconds": 4.2, "busy_loops": 4, "unlimited": 16.32, "half_cpu": 2.29}
}
+15 -13
View File
@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 200" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
<title id="t">CPU time used by four busy loops</title>
<desc id="d">Two bars, CPU seconds consumed in 4.2 seconds of wall time. Without a limit 15.6, with --cpus 0.5 2.4.</desc>
<desc id="d">Two bars, CPU seconds consumed in 4.2 seconds of wall time. Without a limit 16.3, with --cpus 0.5 2.3.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
@@ -21,26 +21,28 @@
</style>
<rect class="bg" x="0" y="0" width="760" height="200" rx="8"/>
<text class="t1" x="20" y="28">CPU time used by 4 busy loops in one container (CPU-seconds)</text>
<text class="t2" x="20" y="46">4.2 s of wall time. Half a CPU allows about 2.1 CPU-seconds; measured 2.4.</text>
<text class="t2" x="20" y="46">4.2 s of wall time. Half a CPU allows about 2.1 CPU-seconds; measured 2.3.</text>
<rect class="oth" x="20" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="38" y="65">no limit</text>
<rect class="acc" x="116" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="134" y="65">--cpus 0.5</text>
<line class="grid" x1="170.0" y1="84" x2="170.0" y2="146"/>
<text class="tick" x="170.0" y="162" text-anchor="middle">0</text>
<line class="grid" x1="295.0" y1="84" x2="295.0" y2="146"/>
<text class="tick" x="295.0" y="162" text-anchor="middle">4</text>
<line class="grid" x1="420.0" y1="84" x2="420.0" y2="146"/>
<text class="tick" x="420.0" y="162" text-anchor="middle">8</text>
<line class="grid" x1="545.0" y1="84" x2="545.0" y2="146"/>
<text class="tick" x="545.0" y="162" text-anchor="middle">12</text>
<line class="grid" x1="270.0" y1="84" x2="270.0" y2="146"/>
<text class="tick" x="270.0" y="162" text-anchor="middle">4</text>
<line class="grid" x1="370.0" y1="84" x2="370.0" y2="146"/>
<text class="tick" x="370.0" y="162" text-anchor="middle">8</text>
<line class="grid" x1="470.0" y1="84" x2="470.0" y2="146"/>
<text class="tick" x="470.0" y="162" text-anchor="middle">12</text>
<line class="grid" x1="570.0" y1="84" x2="570.0" y2="146"/>
<text class="tick" x="570.0" y="162" text-anchor="middle">16</text>
<line class="grid" x1="670.0" y1="84" x2="670.0" y2="146"/>
<text class="tick" x="670.0" y="162" text-anchor="middle">16</text>
<text class="tick" x="670.0" y="162" text-anchor="middle">20</text>
<text class="lab" x="160" y="105.0" text-anchor="end">no limit</text>
<path class="oth" d="M170.0,92.0 h482.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-482.2 z"><title>no limit: 15.6 CPU-seconds</title></path>
<text class="val" x="664.2" y="105.0">15.6 CPU-s</text>
<path class="oth" d="M170.0,92.0 h404.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-404.0 z"><title>no limit: 16.3 CPU-seconds</title></path>
<text class="val" x="586.0" y="105.0">16.3 CPU-s</text>
<text class="lab" x="160" y="135.0" text-anchor="end">--cpus 0.5</text>
<path class="acc" d="M170.0,122.0 h70.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-70.4 z"><title>--cpus 0.5: 2.4 CPU-seconds</title></path>
<text class="val" x="252.4" y="135.0">2.4 CPU-s</text>
<path class="acc" d="M170.0,122.0 h53.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-53.2 z"><title>--cpus 0.5: 2.3 CPU-seconds</title></path>
<text class="val" x="235.2" y="135.0">2.3 CPU-s</text>
<line class="axis" x1="170" y1="84" x2="170" y2="146"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

+3 -3
View File
@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 316" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
<title id="t">Size of the main executable</title>
<desc id="d">Horizontal bars on a logarithmic axis: bubblewrap 79 KiB, crun (OCI runtime) 472 KiB, doos 532 KiB, docker CLI 29.9 MiB, podman 41.2 MiB.</desc>
<desc id="d">Horizontal bars on a logarithmic axis: bubblewrap 79 KiB, crun (OCI runtime) 472 KiB, doos 688 KiB, docker CLI 29.9 MiB, podman 41.2 MiB.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
@@ -42,8 +42,8 @@
<path class="oth" d="M170.0,140.0 h140.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-140.3 z"><title>crun (OCI runtime): 472 KiB</title></path>
<text class="val" x="322.3" y="153.0">472 KiB</text>
<text class="lab" x="160" y="183.0" text-anchor="end">doos</text>
<path class="acc" d="M170.0,170.0 h147.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-147.9 z"><title>doos: 532 KiB</title></path>
<text class="val" x="329.9" y="183.0">532 KiB</text>
<path class="acc" d="M170.0,170.0 h164.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-164.5 z"><title>doos: 688 KiB</title></path>
<text class="val" x="346.5" y="183.0">688 KiB</text>
<text class="lab" x="160" y="213.0" text-anchor="end">docker CLI</text>
<path class="oth" d="M170.0,200.0 h408.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-408.5 z"><title>docker CLI: 29.9 MiB</title></path>
<text class="val" x="590.5" y="213.0">29.9 MiB</text>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+44 -36
View File
@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 502" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
<title id="t">Time to start a trivial command</title>
<desc id="d">Horizontal bars, milliseconds, lower is better. doos: 14 to 82 ms; Podman: 283 to 313 ms; Docker: 463 to 645 ms; bubblewrap: 8 to 9 ms. Exact values are in the table in COMPARISON.md.</desc>
<desc id="d">Horizontal bars, milliseconds, lower is better. doos: 15 to 66 ms; Podman: 216 to 233 ms; Docker: 346 to 449 ms; bubblewrap: 8 to 9 ms. Exact values are in the table in COMPARISON.md.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
@@ -21,62 +21,70 @@
</style>
<rect class="bg" x="0" y="0" width="760" height="502" rx="8"/>
<text class="t1" x="20" y="28">Time to start a trivial command (ms, lower is better)</text>
<text class="t2" x="20" y="46">10 runs each, average, warm cache, 2026-10-24, kernel 7.0.0-38-generic, 12 cores. Docker is a rootful daemon.</text>
<text class="t2" x="20" y="46">10 runs each, average, warm cache, 2026-10-25, kernel 7.0.0-38-generic, 12 cores. Docker is a rootful daemon.</text>
<rect class="acc" x="20" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="38" y="65">doos</text>
<rect class="oth" x="88" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="106" y="65">other tools</text>
<text class="t1" x="22" y="78">No network</text>
<line class="grid" x1="200.0" y1="90" x2="200.0" y2="242"/>
<line class="grid" x1="334.3" y1="90" x2="334.3" y2="242"/>
<line class="grid" x1="468.6" y1="90" x2="468.6" y2="242"/>
<line class="grid" x1="602.9" y1="90" x2="602.9" y2="242"/>
<line class="grid" x1="294.0" y1="90" x2="294.0" y2="242"/>
<line class="grid" x1="388.0" y1="90" x2="388.0" y2="242"/>
<line class="grid" x1="482.0" y1="90" x2="482.0" y2="242"/>
<line class="grid" x1="576.0" y1="90" x2="576.0" y2="242"/>
<line class="grid" x1="670.0" y1="90" x2="670.0" y2="242"/>
<text class="lab" x="190" y="111.0" text-anchor="end">bubblewrap (host /usr)</text>
<path class="oth" d="M200.0,98.0 h1.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-1.9 z"><title>bubblewrap (host /usr): 8.8 ms</title></path>
<text class="val" x="213.9" y="111.0">8.8 ms</text>
<path class="oth" d="M200.0,98.0 h4.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-4.6 z"><title>bubblewrap (host /usr): 9.2 ms</title></path>
<text class="val" x="216.6" y="111.0">9.2 ms</text>
<text class="lab" x="190" y="141.0" text-anchor="end">doos Alpine image</text>
<path class="acc" d="M200.0,128.0 h5.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-5.5 z"><title>doos Alpine image: 14.2 ms</title></path>
<text class="val" x="217.5" y="141.0">14.2 ms</text>
<path class="acc" d="M200.0,128.0 h9.8 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-9.8 z"><title>doos Alpine image: 14.7 ms</title></path>
<text class="val" x="221.8" y="141.0">14.7 ms</text>
<text class="lab" x="190" y="171.0" text-anchor="end">doos host mode</text>
<path class="acc" d="M200.0,158.0 h9.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-9.6 z"><title>doos host mode: 20.2 ms</title></path>
<text class="val" x="221.6" y="171.0">20.2 ms</text>
<path class="acc" d="M200.0,158.0 h11.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-11.2 z"><title>doos host mode: 16.2 ms</title></path>
<text class="val" x="223.2" y="171.0">16.2 ms</text>
<text class="lab" x="190" y="201.0" text-anchor="end">Podman Alpine</text>
<path class="oth" d="M200.0,188.0 h185.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-185.9 z"><title>Podman Alpine: 283 ms</title></path>
<text class="val" x="397.9" y="201.0">283 ms</text>
<path class="oth" d="M200.0,188.0 h199.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-199.0 z"><title>Podman Alpine: 216 ms</title></path>
<text class="val" x="411.0" y="201.0">216 ms</text>
<text class="lab" x="190" y="231.0" text-anchor="end">Docker Alpine (rootful)</text>
<path class="oth" d="M200.0,218.0 h306.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-306.6 z"><title>Docker Alpine (rootful): 463 ms</title></path>
<text class="val" x="518.6" y="231.0">463 ms</text>
<path class="oth" d="M200.0,218.0 h321.1 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-321.1 z"><title>Docker Alpine (rootful): 346 ms</title></path>
<text class="val" x="533.1" y="231.0">346 ms</text>
<line class="axis" x1="200" y1="90" x2="200" y2="242"/>
<text class="tick" x="200.0" y="256" text-anchor="middle">0</text>
<text class="tick" x="334.3" y="256" text-anchor="middle">200</text>
<text class="tick" x="468.6" y="256" text-anchor="middle">400</text>
<text class="tick" x="602.9" y="256" text-anchor="middle">600</text>
<text class="tick" x="294.0" y="256" text-anchor="middle">100</text>
<text class="tick" x="388.0" y="256" text-anchor="middle">200</text>
<text class="tick" x="482.0" y="256" text-anchor="middle">300</text>
<text class="tick" x="576.0" y="256" text-anchor="middle">400</text>
<text class="tick" x="670.0" y="256" text-anchor="middle">500</text>
<text class="t1" x="22" y="280">Default network</text>
<line class="grid" x1="200.0" y1="292" x2="200.0" y2="474"/>
<line class="grid" x1="334.3" y1="292" x2="334.3" y2="474"/>
<line class="grid" x1="468.6" y1="292" x2="468.6" y2="474"/>
<line class="grid" x1="602.9" y1="292" x2="602.9" y2="474"/>
<line class="grid" x1="294.0" y1="292" x2="294.0" y2="474"/>
<line class="grid" x1="388.0" y1="292" x2="388.0" y2="474"/>
<line class="grid" x1="482.0" y1="292" x2="482.0" y2="474"/>
<line class="grid" x1="576.0" y1="292" x2="576.0" y2="474"/>
<line class="grid" x1="670.0" y1="292" x2="670.0" y2="474"/>
<text class="lab" x="190" y="313.0" text-anchor="end">bubblewrap (host /usr)</text>
<path class="oth" d="M200.0,300.0 h1.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-1.2 z"><title>bubblewrap (host /usr): 7.8 ms</title></path>
<text class="val" x="213.2" y="313.0">7.8 ms</text>
<path class="oth" d="M200.0,300.0 h3.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.5 z"><title>bubblewrap (host /usr): 8 ms</title></path>
<text class="val" x="215.5" y="313.0">8 ms</text>
<text class="lab" x="190" y="343.0" text-anchor="end">doos Alpine image</text>
<path class="acc" d="M200.0,330.0 h34.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-34.9 z"><title>doos Alpine image: 58 ms</title></path>
<text class="val" x="246.9" y="343.0">58 ms</text>
<path class="acc" d="M200.0,330.0 h48.8 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-48.8 z"><title>doos Alpine image: 56.2 ms</title></path>
<text class="val" x="260.8" y="343.0">56.2 ms</text>
<text class="lab" x="190" y="373.0" text-anchor="end">doos host mode</text>
<path class="acc" d="M200.0,360.0 h37.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-37.2 z"><title>doos host mode: 61.4 ms</title></path>
<text class="val" x="249.2" y="373.0">61.4 ms</text>
<path class="acc" d="M200.0,360.0 h52.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-52.4 z"><title>doos host mode: 60 ms</title></path>
<text class="val" x="264.4" y="373.0">60 ms</text>
<text class="lab" x="190" y="403.0" text-anchor="end">doos Alpine, pasta</text>
<path class="acc" d="M200.0,390.0 h51.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-51.4 z"><title>doos Alpine, pasta: 82.5 ms</title></path>
<text class="val" x="263.4" y="403.0">82.5 ms</text>
<path class="acc" d="M200.0,390.0 h58.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-58.0 z"><title>doos Alpine, pasta: 66 ms</title></path>
<text class="val" x="270.0" y="403.0">66 ms</text>
<text class="lab" x="190" y="433.0" text-anchor="end">Podman Alpine</text>
<path class="oth" d="M200.0,420.0 h206.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-206.3 z"><title>Podman Alpine: 313 ms</title></path>
<text class="val" x="418.3" y="433.0">313 ms</text>
<path class="oth" d="M200.0,420.0 h214.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-214.9 z"><title>Podman Alpine: 233 ms</title></path>
<text class="val" x="426.9" y="433.0">233 ms</text>
<text class="lab" x="190" y="463.0" text-anchor="end">Docker Alpine (rootful)</text>
<path class="oth" d="M200.0,450.0 h429.1 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-429.1 z"><title>Docker Alpine (rootful): 645 ms</title></path>
<text class="val" x="641.1" y="463.0">645 ms</text>
<path class="oth" d="M200.0,450.0 h418.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-418.2 z"><title>Docker Alpine (rootful): 449 ms</title></path>
<text class="val" x="630.2" y="463.0">449 ms</text>
<line class="axis" x1="200" y1="292" x2="200" y2="474"/>
<text class="tick" x="200.0" y="488" text-anchor="middle">0</text>
<text class="tick" x="334.3" y="488" text-anchor="middle">200</text>
<text class="tick" x="468.6" y="488" text-anchor="middle">400</text>
<text class="tick" x="602.9" y="488" text-anchor="middle">600</text>
<text class="tick" x="294.0" y="488" text-anchor="middle">100</text>
<text class="tick" x="388.0" y="488" text-anchor="middle">200</text>
<text class="tick" x="482.0" y="488" text-anchor="middle">300</text>
<text class="tick" x="576.0" y="488" text-anchor="middle">400</text>
<text class="tick" x="670.0" y="488" text-anchor="middle">500</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB