Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97fa625a50 | ||
|
|
4fed40e5cd |
+32
@@ -1 +1,33 @@
|
||||
# build output
|
||||
build/
|
||||
cmake-build-*/
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
compile_commands.json
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.d
|
||||
|
||||
# state and scratch created by running or testing doos
|
||||
.doos/
|
||||
*.tmp
|
||||
*.log
|
||||
*.pid
|
||||
*.core
|
||||
core
|
||||
|
||||
# editors and OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# local AI tooling state
|
||||
.claude/
|
||||
|
||||
# python bytecode from docs/charts/make_charts.py
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AGENTS.md
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
Rules for any AI agent (or human) changing this repository. The user's global rules
|
||||
also apply; this file adds the project-specific ones. Keep this file current: it is
|
||||
part of every release.
|
||||
@@ -11,8 +13,9 @@ part of every release.
|
||||
extracted image (image mode) with a per-container upper dir, pivots into it and runs a command.
|
||||
Networking is slirp4netns or pasta, ids are mapped through newuidmap when possible, cgroup v2 limits
|
||||
and a watchdog enforce `--memory --pids --cpus --size`, capabilities and seccomp confine the command.
|
||||
No root at runtime. Authoritative behavior: `SPEC.md`. Design notes: `docs/ARCHITECTURE.md`.
|
||||
Measurements and gaps: `docs/RESEARCH.md`.
|
||||
No root at runtime. Authoritative behavior: [SPEC](SPEC.md). Design notes: [ARCHITECTURE](docs/ARCHITECTURE.md).
|
||||
Measurements and gaps: [RESEARCH](docs/RESEARCH.md). Positioning against other tools: [COMPARISON](COMPARISON.md).
|
||||
Usage: [README](README.md). History: [CHANGELOG](CHANGELOG.md).
|
||||
|
||||
## Mandatory release workflow (every change, same session)
|
||||
|
||||
@@ -22,11 +25,12 @@ Measurements and gaps: `docs/RESEARCH.md`.
|
||||
- `patch`: bug fix, docs, tests, tooling, no behavior change for users.
|
||||
- `minor`: new or changed user-visible behavior (flag, command, mount, limit).
|
||||
- `major`: breaking change to CLI, state layout, or container compatibility.
|
||||
4. Run `scripts/release.sh <patch|minor|major> "imperative commit subject"`.
|
||||
4. Run `make docs` (`charts` plus `check-docs`) (the release script does too: every document must link to every other, every relative link must resolve).
|
||||
5. Run `scripts/release.sh <patch|minor|major> "imperative commit subject"`.
|
||||
It bumps `VERSION`, builds, runs `tests/run.sh`, commits all changes, and creates
|
||||
the annotated tag `vX.Y.Z`. It aborts on any build or test failure.
|
||||
5. Verify live: run `build/doos` once against the real thing (the e2e suite does this).
|
||||
6. Report honestly what passed and what did not. Do not push unless the user asks.
|
||||
6. Verify live: run `build/doos` once against the real thing (the e2e suite does this).
|
||||
7. Report honestly what passed and what did not. Do not push unless the user asks.
|
||||
|
||||
Never hand-edit `VERSION` or create tags manually. Never skip the tests to release.
|
||||
|
||||
@@ -42,6 +46,9 @@ Never hand-edit `VERSION` or create tags manually. Never skip the tests to relea
|
||||
| A new measurement | `docs/RESEARCH.md` with machine, kernel, and what was run |
|
||||
| A new known gap or a closed gap | `docs/RESEARCH.md` gaps list and `README.md` |
|
||||
| These workflow rules | this file |
|
||||
| A measurement, or anything the charts show | `make bench` then `make charts` (rewrites `docs/charts/*.svg` and every `<!--bench:...-->` number and table in `COMPARISON.md` and `docs/RESEARCH.md`); never type those numbers by hand |
|
||||
| A feature or limit that appears in the comparison tables | `COMPARISON.md` (tables, the gaps list, the conclusion) |
|
||||
| A new or renamed markdown file | `scripts/check-docs.sh` DOCS list, the nav line (`**Docs:**`) of every document, `README.md` Project docs |
|
||||
| Image catalog, sources, verification, pinned keys | `SPEC.md` Images, `README.md`, `docs/RESEARCH.md` |
|
||||
| Seccomp list, capability set, `/etc` allowlist | `SPEC.md` Hardening and filesystem, `README.md`, `docs/RESEARCH.md` |
|
||||
| Limits, cgroup layout | `SPEC.md` Resource limits, `docs/ARCHITECTURE.md` |
|
||||
@@ -75,6 +82,12 @@ freeze (about 4 minutes in total, needs network, `pasta`, `gpg` and cgroup v2 de
|
||||
before release. On Ubuntu 24.04+ the AppArmor profile in `apparmor/doos` must be
|
||||
installed once for the binary being tested (`doos apparmor`; needs sudo, ask the user, do not do it yourself).
|
||||
|
||||
## Make targets
|
||||
|
||||
`make help` lists them: `build`, `test`, `check-docs`, `bench` (re-measure into `docs/charts/bench.json`),
|
||||
`charts` (SVGs and generated numbers from that file), `docs`, `release KIND=patch MSG="..."`, `clean`.
|
||||
The Makefile is a thin wrapper; the logic lives in `CMakeLists.txt`, `scripts/` and `docs/charts/`.
|
||||
|
||||
## Environment notes
|
||||
|
||||
- State dir: `$DOOS_HOME`, else `$XDG_DATA_HOME/doos`, else `~/.local/share/doos`.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## 1.1.3 - 2026-10-23
|
||||
- Charts: startup time, executable size and CPU limit as SVG (light and dark palettes, doos highlighted), embedded in [COMPARISON](COMPARISON.md) and [RESEARCH](docs/RESEARCH.md).
|
||||
- Reproducible measurements: `scripts/bench.sh` writes `docs/charts/bench.json`; `docs/charts/make_charts.py` builds the charts and rewrites every generated number and table in the documents from it, so text, tables and charts cannot drift apart.
|
||||
- `Makefile` wrapper (`make help`), expanded `.gitignore`.
|
||||
|
||||
## 1.1.2 - 2026-10-23
|
||||
- Docs: new [COMPARISON](COMPARISON.md) (doos against Docker, rootless Docker, Podman, bubblewrap, systemd-nspawn, LXC/Incus, Apptainer, Distrobox/Toolbx, gVisor, Kata/Firecracker) with sourced pros and cons, startup and disk measurements against Docker, Podman and bubblewrap on the same machine, a gaps list tied to code, and a conclusion.
|
||||
- Docs: every document now has a navigation line and links to every other document. `scripts/check-docs.sh` enforces it (and that relative links resolve) and runs inside `scripts/release.sh`.
|
||||
|
||||
## 1.1.1 - 2026-10-23
|
||||
- `doos apparmor` prints a profile for the exact path of the running binary (own profile name per path, so several installs coexist). A start that fails because of AppArmor now prints the install command instead of only `mount bind /: Permission denied`. Verified with a copy in `~/.local/bin`.
|
||||
- `apparmor/doos` (packaging) covers more standard install paths.
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
# 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 | <!--bench:doos_size-->552 KiB<!--/bench-->, <!--bench:shared_libs-->6<!--/bench--> shared libraries, <!--bench:src_lines-->3,445<!--/bench--> lines of C++ [M] | CLI <!--bench:docker_size-->29.9 MiB<!--/bench-->, plus `dockerd` and `containerd` [M][K] | same | <!--bench:podman_size-->41.2 MiB<!--/bench--> [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: a trivial command (`true`) started <!--bench:runs-->10<!--/bench--> times in a row after one warm-up run, average wall time per run, the Alpine image already present for every tool, one machine, one run set on <!--bench:bench_date-->2026-10-23<!--/bench-->. Versions: <!--bench:versions-->doos 1.1.2, Docker 29.1.3, Podman 5.7.0, bubblewrap 0.11.1<!--/bench-->. The numbers come from `scripts/bench.sh` (`make bench`); the charts and the tables below are generated from its output (`make charts`), so they cannot drift from each other.
|
||||
|
||||

|
||||
|
||||
<!-- bench:startup:begin -->
|
||||
| Setup | Network | Time per run |
|
||||
|---|---|---|
|
||||
| bubblewrap (host /usr) | none | 8.4 ms |
|
||||
| doos Alpine image | none | 13.8 ms |
|
||||
| doos host mode | none | 18.7 ms |
|
||||
| Podman Alpine | none | 208 ms |
|
||||
| Docker Alpine (rootful) | none | 344 ms |
|
||||
| bubblewrap (host /usr) | default | 7.4 ms |
|
||||
| doos Alpine image | default | 52.1 ms |
|
||||
| doos host mode | default | 56.9 ms |
|
||||
| doos Alpine, pasta | default | 64.6 ms |
|
||||
| Podman Alpine | default | 241 ms |
|
||||
| Docker Alpine (rootful) | default | 436 ms |
|
||||
<!-- bench:startup:end -->
|
||||
|
||||
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.
|
||||
- Run-to-run spread is real. An earlier ad-hoc run of the same loop measured rootful Docker at 590 ms (no network) and 456 ms (default network); the run charted above measured 344 ms and 436 ms. Read the order of magnitude, not a ranking between close values.
|
||||
- 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.
|
||||
|
||||

|
||||
|
||||
<!-- bench:sizes:begin -->
|
||||
| Executable | Size |
|
||||
|---|---|
|
||||
| bubblewrap | 79 KiB (80,424 bytes) |
|
||||
| crun | 472 KiB (483,392 bytes) |
|
||||
| doos | 552 KiB (565,680 bytes) |
|
||||
| docker CLI | 29.9 MiB (31,374,336 bytes) |
|
||||
| podman | 41.2 MiB (43,180,896 bytes) |
|
||||
<!-- bench:sizes:end -->
|
||||
|
||||
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: <!--bench:doos_size-->552 KiB<!--/bench-->, <!--bench:src_lines-->3,445<!--/bench--> lines of C++ [M], one process tree, no background service, nothing listening.
|
||||
- Instant: <!--bench:doos_range-->14 to 65 ms<!--/bench--> against <!--bench:docker_range-->344 to 436 ms<!--/bench--> for a rootful Docker run and <!--bench:podman_range-->208 to 241 ms<!--/bench--> for Podman [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 <!--bench:doos_size-->552 KiB<!--/bench--> single binary, it starts in <!--bench:doos_range-->14 to 65 ms<!--/bench--> [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.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Thin wrapper over the commands used to build, test and update the project. CMake does the building,
|
||||
# scripts/ and docs/charts/ do the rest. `make help` lists the targets.
|
||||
.DEFAULT_GOAL := help
|
||||
BUILD ?= build
|
||||
DOOS := $(BUILD)/doos
|
||||
|
||||
.PHONY: help build test check-docs bench charts docs release clean
|
||||
|
||||
help: ## list the targets
|
||||
@grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed -E 's/:.*## /\t/' | column -t -s "$$(printf '\t')"
|
||||
|
||||
build: ## configure and compile into build/
|
||||
cmake -S . -B $(BUILD) >/dev/null
|
||||
cmake --build $(BUILD)
|
||||
|
||||
test: build ## end-to-end suite (real downloads, needs network, pasta, gpg, cgroup v2)
|
||||
tests/run.sh $(DOOS)
|
||||
|
||||
check-docs: ## every document links to every other, every relative link resolves
|
||||
scripts/check-docs.sh
|
||||
|
||||
bench: build ## re-measure startup, binary sizes and cpu limit into docs/charts/bench.json
|
||||
scripts/bench.sh
|
||||
|
||||
charts: ## rebuild docs/charts/*.svg and the generated numbers in COMPARISON.md from bench.json
|
||||
python3 docs/charts/make_charts.py
|
||||
|
||||
docs: charts check-docs ## charts plus the docs check
|
||||
|
||||
release: ## KIND=patch|minor|major MSG="subject": bump, build, test, commit, tag
|
||||
@test -n "$(KIND)" -a -n "$(MSG)" || { echo 'usage: make release KIND=patch MSG="subject"'; exit 2; }
|
||||
scripts/release.sh $(KIND) "$(MSG)"
|
||||
|
||||
clean: ## remove build/
|
||||
rm -rf $(BUILD)
|
||||
@@ -1,5 +1,7 @@
|
||||
# doos
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
User-level containers in C++20: overlayfs, user/mount/pid namespaces, slirp4netns or pasta networking, optional images (Alpine, Ubuntu, Debian, or your own rootfs), memory/cpu/pid/size limits, default hardening (capabilities, seccomp). No root at runtime.
|
||||
|
||||
```
|
||||
@@ -11,7 +13,7 @@ doos image list | add [--sha256 HEX] SOURCE [NAME] | delete [--yes] NAME
|
||||
doos apparmor # profile text for this binary (Ubuntu 24.04+)
|
||||
```
|
||||
|
||||
Version: see `VERSION` (currently 1.1.1). Release history: `CHANGELOG.md`.
|
||||
Version: see `VERSION` (currently 1.1.3). Release history: `CHANGELOG.md`.
|
||||
|
||||
User-level containers in C++20: overlayfs over the host's `/usr`, `/etc`, `/opt`, user/mount/pid namespaces, networking through slirp4netns. No root at runtime.
|
||||
|
||||
@@ -166,12 +168,19 @@ run with Docker's default capability set and a seccomp filter. Memory, pids, cpu
|
||||
`--net host` removes network isolation and is opt-in (`--net pasta` is the safer host-like choice).
|
||||
What is still not covered is listed at the end of `docs/RESEARCH.md`.
|
||||
|
||||
## Development
|
||||
|
||||
`make help` lists the targets: `make build`, `make test` (end-to-end, needs network), `make bench` (re-measure startup, sizes and the cpu limit), `make charts` (rebuild the SVG charts and the generated numbers in the comparison), `make docs`, `make release KIND=patch MSG="..."`. Build and test details are in [AGENTS](AGENTS.md); the charts and how they are generated are in [COMPARISON](COMPARISON.md) section 3 and `docs/charts/`.
|
||||
|
||||
## Project docs
|
||||
|
||||
| File | Content |
|
||||
|---|---|
|
||||
| `SPEC.md` | Behavior contract and limits |
|
||||
| `docs/ARCHITECTURE.md` | Code map and flows |
|
||||
| `docs/RESEARCH.md` | What works unprivileged, measurements, gaps |
|
||||
| `AGENTS.md` | Rules for AI agents, release workflow |
|
||||
| `CHANGELOG.md` | Release history |
|
||||
| [`SPEC.md`](SPEC.md) | Behavior contract and limits |
|
||||
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Code map and flows |
|
||||
| [`docs/RESEARCH.md`](docs/RESEARCH.md) | What works unprivileged, measurements, remaining limits |
|
||||
| [`COMPARISON.md`](COMPARISON.md) | doos next to Docker, Podman, bubblewrap, nspawn, LXC/Incus, Apptainer, Distrobox, gVisor/Kata, with pros, cons, measurements and a conclusion |
|
||||
| [`AGENTS.md`](AGENTS.md) | Rules for AI agents, release workflow |
|
||||
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
|
||||
|
||||
Not sure whether doos is the right tool? Read the conclusion of [COMPARISON](COMPARISON.md) first.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# doos specification
|
||||
|
||||
Authoritative description of behavior and limits. Code constants live in `src/spec.hpp`; change both together.
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
Authoritative description of behavior and limits. Code constants live in `src/spec.hpp`; change both together. How the code implements it: [ARCHITECTURE](docs/ARCHITECTURE.md). Measurements and remaining limits: [RESEARCH](docs/RESEARCH.md). How it differs from other tools: [COMPARISON](COMPARISON.md). Usage: [README](README.md).
|
||||
|
||||
## CLI
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Architecture
|
||||
|
||||
How doos works internally. Behavior contract is `SPEC.md`; this explains the code.
|
||||
**Docs:** [README](../README.md) · [SPEC](../SPEC.md) · [ARCHITECTURE](ARCHITECTURE.md) · [RESEARCH](RESEARCH.md) · [COMPARISON](../COMPARISON.md) · [AGENTS](../AGENTS.md) · [CHANGELOG](../CHANGELOG.md)
|
||||
|
||||
How doos works internally. The behavior contract is [SPEC](../SPEC.md); this explains the code. Why it was built this way and what was measured: [RESEARCH](RESEARCH.md); how it compares with other tools: [COMPARISON](../COMPARISON.md).
|
||||
|
||||
## File map
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Research: user-level containers with overlayfs
|
||||
|
||||
**Docs:** [README](../README.md) · [SPEC](../SPEC.md) · [ARCHITECTURE](ARCHITECTURE.md) · [RESEARCH](RESEARCH.md) · [COMPARISON](../COMPARISON.md) · [AGENTS](../AGENTS.md) · [CHANGELOG](../CHANGELOG.md)
|
||||
|
||||
Measured on this machine: Ubuntu, kernel 7.0.0-38, AppArmor 5.0.2, slirp4netns 1.3.3, uid 1000, no root at runtime.
|
||||
|
||||
## What works unprivileged
|
||||
@@ -258,6 +260,10 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
|
||||
| Size accounting | 80 MB written by container uid 5 in a mode-700 directory (invisible to the host user) against `--size 64m`: frozen; cleanup from a second attach thawed it |
|
||||
| Attach time (5 runs) | host or image, slirp: about 40 ms each; pasta: about 65 ms; `--net none`: about 10 ms |
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
- Hardening can be switched off per container (`--hardening relaxed`), so nothing that needs the denied calls is lost, it is opt-in now.
|
||||
@@ -278,3 +284,5 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
|
||||
- Without `gpg` installed, signature checks are skipped with a printed note.
|
||||
- pasta is not signalled by doos (it drops uid); it exits by itself within about a second after the container's namespace is gone. Measured: no pasta process remained after 1.5 s.
|
||||
- seccomp is a denylist. A new kernel syscall is allowed until it is added to the list.
|
||||
|
||||
For how doos stacks up against Docker, Podman and others, with these measurements next to theirs, see [COMPARISON](../COMPARISON.md). Behavior contract: [SPEC](../SPEC.md). Code map: [ARCHITECTURE](ARCHITECTURE.md). Usage: [README](../README.md).
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"meta": {
|
||||
"date": "2026-10-23",
|
||||
"kernel": "7.0.0-38-generic",
|
||||
"cpus": 12,
|
||||
"runs": 10,
|
||||
"doos": "1.1.2",
|
||||
"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": 3445,
|
||||
"shared_libs": 6
|
||||
},
|
||||
"startup": [
|
||||
{
|
||||
"id": "bwrap_none",
|
||||
"label": "bubblewrap (host /usr)",
|
||||
"group": "none",
|
||||
"kind": "other",
|
||||
"ms": 8.4
|
||||
},
|
||||
{
|
||||
"id": "bwrap_net",
|
||||
"label": "bubblewrap (host /usr)",
|
||||
"group": "default",
|
||||
"kind": "other",
|
||||
"ms": 7.4
|
||||
},
|
||||
{
|
||||
"id": "doos_host_none",
|
||||
"label": "doos host mode",
|
||||
"group": "none",
|
||||
"kind": "doos",
|
||||
"ms": 18.7
|
||||
},
|
||||
{
|
||||
"id": "doos_alpine_none",
|
||||
"label": "doos Alpine image",
|
||||
"group": "none",
|
||||
"kind": "doos",
|
||||
"ms": 13.8
|
||||
},
|
||||
{
|
||||
"id": "doos_host_slirp",
|
||||
"label": "doos host mode",
|
||||
"group": "default",
|
||||
"kind": "doos",
|
||||
"ms": 56.9
|
||||
},
|
||||
{
|
||||
"id": "doos_alpine_slirp",
|
||||
"label": "doos Alpine image",
|
||||
"group": "default",
|
||||
"kind": "doos",
|
||||
"ms": 52.1
|
||||
},
|
||||
{
|
||||
"id": "doos_alpine_pasta",
|
||||
"label": "doos Alpine, pasta",
|
||||
"group": "default",
|
||||
"kind": "doos",
|
||||
"ms": 64.6
|
||||
},
|
||||
{
|
||||
"id": "podman_none",
|
||||
"label": "Podman Alpine",
|
||||
"group": "none",
|
||||
"kind": "other",
|
||||
"ms": 207.8
|
||||
},
|
||||
{
|
||||
"id": "podman_net",
|
||||
"label": "Podman Alpine",
|
||||
"group": "default",
|
||||
"kind": "other",
|
||||
"ms": 241.2
|
||||
},
|
||||
{
|
||||
"id": "docker_none",
|
||||
"label": "Docker Alpine (rootful)",
|
||||
"group": "none",
|
||||
"kind": "other",
|
||||
"ms": 343.9
|
||||
},
|
||||
{
|
||||
"id": "docker_net",
|
||||
"label": "Docker Alpine (rootful)",
|
||||
"group": "default",
|
||||
"kind": "other",
|
||||
"ms": 435.6
|
||||
}
|
||||
],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "bwrap",
|
||||
"bytes": 80424
|
||||
},
|
||||
{
|
||||
"id": "crun",
|
||||
"bytes": 483392
|
||||
},
|
||||
{
|
||||
"id": "doos",
|
||||
"bytes": 565680
|
||||
},
|
||||
{
|
||||
"id": "docker",
|
||||
"bytes": 31374336
|
||||
},
|
||||
{
|
||||
"id": "podman",
|
||||
"bytes": 43180896
|
||||
}
|
||||
],
|
||||
"cpu": {
|
||||
"wall_seconds": 4.2,
|
||||
"busy_loops": 4,
|
||||
"unlimited": 16.26,
|
||||
"half_cpu": 2.29
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<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 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; }
|
||||
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.grid { stroke: var(--grid); stroke-width: 1; }
|
||||
.axis { stroke: var(--axis); stroke-width: 1; }
|
||||
.acc { fill: var(--accent); }
|
||||
.oth { fill: var(--other); }
|
||||
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
||||
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
|
||||
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
|
||||
}
|
||||
</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.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="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">20</text>
|
||||
<text class="lab" x="160" y="105.0" text-anchor="end">no limit</text>
|
||||
<path class="oth" d="M170.0,92.0 h402.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-402.5 z"><title>no limit: 16.3 CPU-seconds</title></path>
|
||||
<text class="val" x="584.5" 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 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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Builds the SVG charts and the generated numbers in COMPARISON.md / RESEARCH.md from bench.json.
|
||||
|
||||
python3 docs/charts/make_charts.py (or: make charts)
|
||||
|
||||
Input : docs/charts/bench.json (written by scripts/bench.sh, i.e. make bench)
|
||||
Output: docs/charts/startup.svg, size.svg, cpu.svg
|
||||
the blocks between <!-- bench:NAME:begin --> and <!-- bench:NAME:end --> in the markdown documents
|
||||
and inline values written as <!--bench:KEY-->text<!--/bench-->
|
||||
|
||||
Design rules (the dataviz method): one form per job, doos in the accent hue and everything else in the neutral
|
||||
gray (emphasis), bars at most 24 px with a 4 px rounded data end, direct value labels at the bar tip, a legend,
|
||||
hairline solid grid, text in ink tokens (never the series color), light and dark palettes selected separately.
|
||||
Colors were checked with the dataviz validator: contrast, CVD separation and the normal-vision floor pass in
|
||||
both modes; its "chroma floor" flag on the gray is expected, the gray is the de-emphasis neutral by design.
|
||||
Standard library only.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
DATA = HERE / "bench.json"
|
||||
|
||||
# ---- palette (dataviz reference instance) ---------------------------------------------------------------
|
||||
LIGHT = dict(surface="#fcfcfb", ink="#0b0b0b", ink2="#52514e", muted="#898781", grid="#e1e0d9", axis="#c3c2b7",
|
||||
accent="#2a78d6", other="#898781")
|
||||
DARK = dict(surface="#1a1a19", ink="#f0efec", ink2="#c3c2b7", muted="#898781", grid="#2c2c2a", axis="#383835",
|
||||
accent="#3987e5", other="#898781")
|
||||
|
||||
STYLE = f"""
|
||||
.bg {{ fill: var(--surface); }}
|
||||
.t1 {{ fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.t2 {{ fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.lab {{ fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.val {{ fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.tick {{ fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.grid {{ stroke: var(--grid); stroke-width: 1; }}
|
||||
.axis {{ stroke: var(--axis); stroke-width: 1; }}
|
||||
.acc {{ fill: var(--accent); }}
|
||||
.oth {{ fill: var(--other); }}
|
||||
:root {{ --surface:{LIGHT['surface']}; --ink:{LIGHT['ink']}; --ink2:{LIGHT['ink2']}; --muted:{LIGHT['muted']};
|
||||
--grid:{LIGHT['grid']}; --axis:{LIGHT['axis']}; --accent:{LIGHT['accent']}; --other:{LIGHT['other']}; }}
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
:root {{ --surface:{DARK['surface']}; --ink:{DARK['ink']}; --ink2:{DARK['ink2']}; --muted:{DARK['muted']};
|
||||
--grid:{DARK['grid']}; --axis:{DARK['axis']}; --accent:{DARK['accent']}; --other:{DARK['other']}; }}
|
||||
}}
|
||||
"""
|
||||
|
||||
BAR_H = 18 # <= 24 px
|
||||
ROW = 30
|
||||
RADIUS = 4 # rounded data end only
|
||||
|
||||
|
||||
def bar_path(x, y, w, h, r=RADIUS):
|
||||
"""Bar growing from x to the right: square at the baseline, rounded at the data end."""
|
||||
w = max(w, 1.0)
|
||||
r = min(r, w, h / 2)
|
||||
return (f"M{x:.1f},{y:.1f} h{w - r:.1f} a{r},{r} 0 0 1 {r},{r} v{h - 2 * r:.1f} "
|
||||
f"a{r},{r} 0 0 1 {-r},{r} h{-(w - r):.1f} z")
|
||||
|
||||
|
||||
def svg_open(w, h, title, desc):
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" role="img" '
|
||||
f'aria-labelledby="t d" style="max-width:100%;height:auto">\n<title id="t">{escape(title)}</title>\n'
|
||||
f'<desc id="d">{escape(desc)}</desc>\n<style>{STYLE}</style>\n'
|
||||
f'<rect class="bg" x="0" y="0" width="{w}" height="{h}" rx="8"/>\n')
|
||||
|
||||
|
||||
def legend(x, y, items):
|
||||
out, cx = [], x
|
||||
for cls, text in items:
|
||||
out.append(f'<rect class="{cls}" x="{cx}" y="{y - 9}" width="12" height="12" rx="2"/>')
|
||||
out.append(f'<text class="t2" x="{cx + 18}" y="{y + 1}">{escape(text)}</text>')
|
||||
cx += 18 + 7 * len(text) + 22
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def fmt_ms(v):
|
||||
return f"{v:.0f} ms" if v >= 100 else f"{v:.1f} ms".replace(".0 ms", " ms")
|
||||
|
||||
|
||||
def fmt_bytes(b):
|
||||
if b < 1 << 20:
|
||||
return f"{b / 1024:.0f} KiB"
|
||||
return f"{b / (1 << 20):.1f} MiB"
|
||||
|
||||
|
||||
# ---- chart 1: startup time -----------------------------------------------------------------------------
|
||||
def startup_chart(d):
|
||||
rows = [r for r in d["startup"] if r["ms"] is not None]
|
||||
iv = inline_values(d)
|
||||
groups = [("none", "No network"), ("default", "Default network")]
|
||||
left, right, top = 200, 90, 78
|
||||
width = 760
|
||||
plot_w = width - left - right
|
||||
vmax = max(r["ms"] for r in rows)
|
||||
axis_max = math.ceil(vmax / 100.0) * 100
|
||||
step = 100 if axis_max <= 600 else 200
|
||||
scale = plot_w / axis_max
|
||||
|
||||
y = top
|
||||
body = []
|
||||
for key, name in groups:
|
||||
grp = sorted([r for r in rows if r["group"] == key], key=lambda r: r["ms"])
|
||||
if not grp:
|
||||
continue
|
||||
body.append(f'<text class="t1" x="{left - 190 + 12}" y="{y}">{name}</text>')
|
||||
y += 14
|
||||
y0 = y
|
||||
for t in range(0, axis_max + 1, step):
|
||||
x = left + t * scale
|
||||
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(grp) * ROW}"/>')
|
||||
for r in grp:
|
||||
cls = "acc" if r["kind"] == "doos" else "oth"
|
||||
w = r["ms"] * scale
|
||||
by = y + (ROW - BAR_H) / 2
|
||||
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{escape(r["label"])}</text>')
|
||||
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{escape(r["label"])}: {fmt_ms(r["ms"])}</title></path>')
|
||||
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{fmt_ms(r["ms"])}</text>')
|
||||
y += ROW
|
||||
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
|
||||
for t in range(0, axis_max + 1, step):
|
||||
body.append(f'<text class="tick" x="{left + t * scale:.1f}" y="{y + 14}" text-anchor="middle">{t}</text>')
|
||||
y += 38
|
||||
height = int(y - 10)
|
||||
m = d["meta"]
|
||||
out = svg_open(width, height, "Time to start a trivial command",
|
||||
f"Horizontal bars, milliseconds, lower is better. doos: {iv['doos_range']}; Podman: {iv['podman_range']}; "
|
||||
f"Docker: {iv['docker_range']}; bubblewrap: {iv['bwrap_range']}. Exact values are in the table in COMPARISON.md.")
|
||||
out += '<text class="t1" x="20" y="28">Time to start a trivial command (ms, lower is better)</text>\n'
|
||||
out += (f'<text class="t2" x="20" y="46">{m["runs"]} runs each, average, warm cache, {m["date"]}, kernel {escape(m["kernel"])}, '
|
||||
f'{m["cpus"]} cores. Docker is a rootful daemon.</text>\n')
|
||||
out += legend(20, 64, [("acc", "doos"), ("oth", "other tools")]) + "\n"
|
||||
out += "\n".join(body) + "\n</svg>\n"
|
||||
return out
|
||||
|
||||
|
||||
# ---- chart 2: binary size (log scale) --------------------------------------------------------------------
|
||||
def size_chart(d):
|
||||
names = {"bwrap": "bubblewrap", "crun": "crun (OCI runtime)", "doos": "doos", "docker": "docker CLI",
|
||||
"podman": "podman"}
|
||||
rows = sorted([r for r in d["sizes"] if r["bytes"] is not None], key=lambda r: r["bytes"])
|
||||
left, right, top, width = 170, 100, 96, 760
|
||||
plot_w = width - left - right
|
||||
lo, hi = math.log10(50 * 1024), math.log10(100 * (1 << 20))
|
||||
sx = lambda b: (math.log10(b) - lo) / (hi - lo) * plot_w
|
||||
ticks = [(100 * 1024, "100 KiB"), (1 << 20, "1 MiB"), (10 << 20, "10 MiB"), (100 << 20, "100 MiB")]
|
||||
y0 = top + 8
|
||||
body = []
|
||||
for b, lab in ticks:
|
||||
x = left + sx(b)
|
||||
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(rows) * ROW}"/>')
|
||||
body.append(f'<text class="tick" x="{x:.1f}" y="{y0 + len(rows) * ROW + 16}" text-anchor="middle">{lab}</text>')
|
||||
y = y0
|
||||
for r in rows:
|
||||
cls = "acc" if r["id"] == "doos" else "oth"
|
||||
w = sx(r["bytes"])
|
||||
by = y + (ROW - BAR_H) / 2
|
||||
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{names.get(r["id"], r["id"])}</text>')
|
||||
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{names.get(r["id"], r["id"])}: {fmt_bytes(r["bytes"])}</title></path>')
|
||||
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{fmt_bytes(r["bytes"])}</text>')
|
||||
y += ROW
|
||||
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
|
||||
height = int(y + 62)
|
||||
out = svg_open(width, height, "Size of the main executable",
|
||||
"Horizontal bars on a logarithmic axis: " +
|
||||
", ".join(f"{names.get(r['id'], r['id'])} {fmt_bytes(r['bytes'])}" for r in rows) + ".")
|
||||
out += '<text class="t1" x="20" y="28">Size of the main executable (log scale, smaller is smaller)</text>\n'
|
||||
out += '<text class="t2" x="20" y="46">The docker CLI excludes dockerd and containerd.</text>\n'
|
||||
out += '<text class="t2" x="20" y="62">doos excludes the helpers it calls (slirp4netns, pasta, newuidmap, curl, tar).</text>\n'
|
||||
out += legend(20, 82, [("acc", "doos"), ("oth", "other tools")]) + "\n"
|
||||
out += "\n".join(body) + f'\n<text class="t2" x="{left}" y="{y + 38}">Axis is logarithmic: each gridline is 10x the previous one.</text>\n</svg>\n'
|
||||
return out
|
||||
|
||||
|
||||
# ---- chart 3: cpu limit ----------------------------------------------------------------------------------
|
||||
def cpu_chart(d):
|
||||
c = d["cpu"]
|
||||
if c["unlimited"] is None or c["half_cpu"] is None:
|
||||
return None
|
||||
rows = [("no limit", c["unlimited"], "oth"), ("--cpus 0.5", c["half_cpu"], "acc")]
|
||||
left, right, top, width = 170, 90, 78, 760
|
||||
plot_w = width - left - right
|
||||
axis_max = math.ceil(c["unlimited"] / 4.0) * 4
|
||||
scale = plot_w / axis_max
|
||||
y0 = top + 8
|
||||
body = []
|
||||
for t in range(0, axis_max + 1, 4):
|
||||
x = left + t * scale
|
||||
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(rows) * ROW}"/>')
|
||||
body.append(f'<text class="tick" x="{x:.1f}" y="{y0 + len(rows) * ROW + 16}" text-anchor="middle">{t}</text>')
|
||||
y = y0
|
||||
for lab, v, cls in rows:
|
||||
w = v * scale
|
||||
by = y + (ROW - BAR_H) / 2
|
||||
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{lab}</text>')
|
||||
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{lab}: {v:.1f} CPU-seconds</title></path>')
|
||||
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{v:.1f} CPU-s</text>')
|
||||
y += ROW
|
||||
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
|
||||
ideal = 0.5 * c["wall_seconds"]
|
||||
height = int(y + 54)
|
||||
out = svg_open(width, height, "CPU time used by four busy loops",
|
||||
f"Two bars, CPU seconds consumed in {c['wall_seconds']} seconds of wall time. Without a limit {c['unlimited']:.1f}, "
|
||||
f"with --cpus 0.5 {c['half_cpu']:.1f}.")
|
||||
out += '<text class="t1" x="20" y="28">CPU time used by 4 busy loops in one container (CPU-seconds)</text>\n'
|
||||
out += (f'<text class="t2" x="20" y="46">{c["wall_seconds"]} s of wall time. Half a CPU allows about {ideal:.1f} CPU-seconds; '
|
||||
f'measured {c["half_cpu"]:.1f}.</text>\n')
|
||||
out += legend(20, 64, [("oth", "no limit"), ("acc", "--cpus 0.5")]) + "\n"
|
||||
out += "\n".join(body) + "\n</svg>\n"
|
||||
return out
|
||||
|
||||
|
||||
# ---- generated markdown ---------------------------------------------------------------------------------
|
||||
def startup_table(d):
|
||||
rows = [r for r in d["startup"] if r["ms"] is not None]
|
||||
lines = ["| Setup | Network | Time per run |", "|---|---|---|"]
|
||||
order = {"none": 0, "default": 1}
|
||||
for r in sorted(rows, key=lambda r: (order[r["group"]], r["ms"])):
|
||||
net = "none" if r["group"] == "none" else "default"
|
||||
lines.append(f"| {r['label']} | {net} | {fmt_ms(r['ms'])} |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def size_table(d):
|
||||
names = {"bwrap": "bubblewrap", "crun": "crun", "doos": "doos", "docker": "docker CLI", "podman": "podman"}
|
||||
lines = ["| Executable | Size |", "|---|---|"]
|
||||
for r in sorted([r for r in d["sizes"] if r["bytes"] is not None], key=lambda r: r["bytes"]):
|
||||
lines.append(f"| {names.get(r['id'], r['id'])} | {fmt_bytes(r['bytes'])} ({r['bytes']:,} bytes) |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def inline_values(d):
|
||||
ms = lambda ids: [r["ms"] for r in d["startup"] if r["id"].startswith(ids) and r["ms"] is not None]
|
||||
rng = lambda xs: f"{min(xs):.0f} to {max(xs):.0f} ms" if xs else "n/a"
|
||||
size = {r["id"]: r["bytes"] for r in d["sizes"] if r["bytes"] is not None}
|
||||
v = {
|
||||
"doos_range": rng(ms("doos")),
|
||||
"podman_range": rng(ms("podman")),
|
||||
"docker_range": rng(ms("docker")),
|
||||
"bwrap_range": rng(ms("bwrap")),
|
||||
"doos_size": fmt_bytes(size["doos"]) if "doos" in size else "n/a",
|
||||
"docker_size": fmt_bytes(size["docker"]) if "docker" in size else "n/a",
|
||||
"podman_size": fmt_bytes(size["podman"]) if "podman" in size else "n/a",
|
||||
"bench_date": d["meta"]["date"],
|
||||
"src_lines": f'{d["meta"].get("src_lines", 0):,}',
|
||||
"shared_libs": str(d["meta"].get("shared_libs", "n/a")),
|
||||
"runs": str(d["meta"]["runs"]),
|
||||
"versions": f'doos {d["meta"]["doos"]}, {d["meta"]["docker"].replace("Docker version ", "Docker ").split(",")[0]}, '
|
||||
f'{d["meta"]["podman"].replace("podman version ", "Podman ")}, {d["meta"]["bubblewrap"].replace("bubblewrap ", "bubblewrap ")}',
|
||||
"cpu_free": f'{d["cpu"]["unlimited"]:.1f}' if d["cpu"]["unlimited"] is not None else "n/a",
|
||||
"cpu_half": f'{d["cpu"]["half_cpu"]:.1f}' if d["cpu"]["half_cpu"] is not None else "n/a",
|
||||
}
|
||||
return v
|
||||
|
||||
|
||||
def patch_markdown(path, blocks, inline):
|
||||
s = path.read_text()
|
||||
orig = s
|
||||
for name, text in blocks.items():
|
||||
pat = re.compile(rf"(<!-- bench:{name}:begin -->\n).*?(\n<!-- bench:{name}:end -->)", re.S)
|
||||
s = pat.sub(lambda m: m.group(1) + text + m.group(2), s)
|
||||
s = re.sub(r"<!--bench:(\w+)-->.*?<!--/bench-->",
|
||||
lambda m: f"<!--bench:{m.group(1)}-->{inline.get(m.group(1), 'n/a')}<!--/bench-->", s)
|
||||
if s != orig:
|
||||
path.write_text(s)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if not DATA.exists():
|
||||
sys.exit("no bench.json: run `make bench` first (it needs the built binary)")
|
||||
d = json.loads(DATA.read_text())
|
||||
(HERE / "startup.svg").write_text(startup_chart(d))
|
||||
(HERE / "size.svg").write_text(size_chart(d))
|
||||
cpu = cpu_chart(d)
|
||||
if cpu:
|
||||
(HERE / "cpu.svg").write_text(cpu)
|
||||
blocks = {"startup": startup_table(d), "sizes": size_table(d)}
|
||||
inline = inline_values(d)
|
||||
changed = [p.name for p in (ROOT / "COMPARISON.md", ROOT / "docs" / "RESEARCH.md") if patch_markdown(p, blocks, inline)]
|
||||
print("charts: startup.svg size.svg" + (" cpu.svg" if cpu else "") + (f"; updated {', '.join(changed)}" if changed else "; docs already current"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
<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 552 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; }
|
||||
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.grid { stroke: var(--grid); stroke-width: 1; }
|
||||
.axis { stroke: var(--axis); stroke-width: 1; }
|
||||
.acc { fill: var(--accent); }
|
||||
.oth { fill: var(--other); }
|
||||
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
||||
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
|
||||
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
|
||||
}
|
||||
</style>
|
||||
<rect class="bg" x="0" y="0" width="760" height="316" rx="8"/>
|
||||
<text class="t1" x="20" y="28">Size of the main executable (log scale, smaller is smaller)</text>
|
||||
<text class="t2" x="20" y="46">The docker CLI excludes dockerd and containerd.</text>
|
||||
<text class="t2" x="20" y="62">doos excludes the helpers it calls (slirp4netns, pasta, newuidmap, curl, tar).</text>
|
||||
<rect class="acc" x="20" y="73" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="38" y="83">doos</text>
|
||||
<rect class="oth" x="88" y="73" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="106" y="83">other tools</text>
|
||||
<line class="grid" x1="214.5" y1="102" x2="214.5" y2="254"/>
|
||||
<text class="tick" x="214.5" y="270" text-anchor="middle">100 KiB</text>
|
||||
<line class="grid" x1="364.0" y1="102" x2="364.0" y2="254"/>
|
||||
<text class="tick" x="364.0" y="270" text-anchor="middle">1 MiB</text>
|
||||
<line class="grid" x1="512.0" y1="102" x2="512.0" y2="254"/>
|
||||
<text class="tick" x="512.0" y="270" text-anchor="middle">10 MiB</text>
|
||||
<line class="grid" x1="660.0" y1="102" x2="660.0" y2="254"/>
|
||||
<text class="tick" x="660.0" y="270" text-anchor="middle">100 MiB</text>
|
||||
<text class="lab" x="160" y="123.0" text-anchor="end">bubblewrap</text>
|
||||
<path class="oth" d="M170.0,110.0 h25.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-25.0 z"><title>bubblewrap: 79 KiB</title></path>
|
||||
<text class="val" x="207.0" y="123.0">79 KiB</text>
|
||||
<text class="lab" x="160" y="153.0" text-anchor="end">crun (OCI runtime)</text>
|
||||
<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 h150.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-150.4 z"><title>doos: 552 KiB</title></path>
|
||||
<text class="val" x="332.4" y="183.0">552 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>
|
||||
<text class="lab" x="160" y="243.0" text-anchor="end">podman</text>
|
||||
<path class="oth" d="M170.0,230.0 h429.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-429.0 z"><title>podman: 41.2 MiB</title></path>
|
||||
<text class="val" x="611.0" y="243.0">41.2 MiB</text>
|
||||
<line class="axis" x1="170" y1="102" x2="170" y2="254"/>
|
||||
<text class="t2" x="170" y="292">Axis is logarithmic: each gridline is 10x the previous one.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,90 @@
|
||||
<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 65 ms; Podman: 208 to 241 ms; Docker: 344 to 436 ms; bubblewrap: 7 to 8 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; }
|
||||
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.grid { stroke: var(--grid); stroke-width: 1; }
|
||||
.axis { stroke: var(--axis); stroke-width: 1; }
|
||||
.acc { fill: var(--accent); }
|
||||
.oth { fill: var(--other); }
|
||||
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
||||
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
|
||||
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
|
||||
}
|
||||
</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-23, 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="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 h3.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.9 z"><title>bubblewrap (host /usr): 8.4 ms</title></path>
|
||||
<text class="val" x="215.9" y="111.0">8.4 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 h9.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-9.0 z"><title>doos Alpine image: 13.8 ms</title></path>
|
||||
<text class="val" x="221.0" y="141.0">13.8 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 h13.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-13.6 z"><title>doos host mode: 18.7 ms</title></path>
|
||||
<text class="val" x="225.6" y="171.0">18.7 ms</text>
|
||||
<text class="lab" x="190" y="201.0" text-anchor="end">Podman Alpine</text>
|
||||
<path class="oth" d="M200.0,188.0 h191.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-191.3 z"><title>Podman Alpine: 208 ms</title></path>
|
||||
<text class="val" x="403.3" y="201.0">208 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 h319.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-319.3 z"><title>Docker Alpine (rootful): 344 ms</title></path>
|
||||
<text class="val" x="531.3" y="231.0">344 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="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="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 h3.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.0 z"><title>bubblewrap (host /usr): 7.4 ms</title></path>
|
||||
<text class="val" x="215.0" y="313.0">7.4 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 h45.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-45.0 z"><title>doos Alpine image: 52.1 ms</title></path>
|
||||
<text class="val" x="257.0" y="343.0">52.1 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 h49.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-49.5 z"><title>doos host mode: 56.9 ms</title></path>
|
||||
<text class="val" x="261.5" y="373.0">56.9 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 h56.7 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-56.7 z"><title>doos Alpine, pasta: 64.6 ms</title></path>
|
||||
<text class="val" x="268.7" y="403.0">64.6 ms</text>
|
||||
<text class="lab" x="190" y="433.0" text-anchor="end">Podman Alpine</text>
|
||||
<path class="oth" d="M200.0,420.0 h222.7 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-222.7 z"><title>Podman Alpine: 241 ms</title></path>
|
||||
<text class="val" x="434.7" y="433.0">241 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 h405.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-405.5 z"><title>Docker Alpine (rootful): 436 ms</title></path>
|
||||
<text class="val" x="617.5" y="463.0">436 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="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>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducible measurements behind COMPARISON.md and the charts. Writes JSON (default docs/charts/bench.json).
|
||||
# Tools that are not installed or not usable are skipped and recorded as null, never guessed.
|
||||
# Usage: scripts/bench.sh [output.json] Environment: BENCH_RUNS (default 10)
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=${1:-docs/charts/bench.json}
|
||||
RUNS=${BENCH_RUNS:-10}
|
||||
DOOS=$(readlink -f build/doos)
|
||||
[ -x "$DOOS" ] || { echo "bench: build first (make build)" >&2; exit 1; }
|
||||
export DOOS_HOME=$(mktemp -d)
|
||||
trap '"$DOOS" delete --yes ha >/dev/null 2>&1; rm -rf "$DOOS_HOME"' EXIT
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
now() { date +%s%N; }
|
||||
# avg_ms LABEL CMD... -> prints average ms per run, or "null" when the command fails
|
||||
avg_ms() {
|
||||
timeout 180 "$@" >/dev/null 2>&1 || { echo null; return; } # warm-up, may pull an image
|
||||
local s e i; s=$(now)
|
||||
for i in $(seq "$RUNS"); do timeout 60 "$@" >/dev/null 2>&1 || { echo null; return; }; done
|
||||
e=$(now); awk -v s="$s" -v e="$e" -v n="$RUNS" 'BEGIN{printf "%.1f", (e-s)/1e6/n}'
|
||||
}
|
||||
note() { echo "bench: $*" >&2; }
|
||||
|
||||
# ---- setup doos containers (host mode needs nothing, image mode downloads Alpine once)
|
||||
"$DOOS" image add alpine >/dev/null 2>&1 || note "alpine image unavailable, image-mode rows will be null"
|
||||
"$DOOS" get_or_create --net none hn >/dev/null 2>&1
|
||||
"$DOOS" get_or_create hs >/dev/null 2>&1
|
||||
"$DOOS" get_or_create --image alpine --net none an >/dev/null 2>&1
|
||||
"$DOOS" get_or_create --image alpine as >/dev/null 2>&1
|
||||
"$DOOS" get_or_create --image alpine --net pasta ap >/dev/null 2>&1
|
||||
|
||||
BW_BASE="--unshare-user --unshare-pid --unshare-ipc --unshare-uts --die-with-parent --ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 --proc /proc --dev /dev"
|
||||
rows=()
|
||||
add() { rows+=("{\"id\":\"$1\",\"label\":\"$2\",\"group\":\"$3\",\"kind\":\"$4\",\"ms\":$5}"); }
|
||||
|
||||
echo "bench: startup, $RUNS runs each" >&2
|
||||
if have bwrap; then
|
||||
add bwrap_none "bubblewrap (host /usr)" none other "$(avg_ms bwrap --unshare-all $BW_BASE true)"
|
||||
add bwrap_net "bubblewrap (host /usr)" default other "$(avg_ms bwrap $BW_BASE true)"
|
||||
fi
|
||||
add doos_host_none "doos host mode" none doos "$(avg_ms "$DOOS" attach hn -- true)"
|
||||
add doos_alpine_none "doos Alpine image" none doos "$(avg_ms "$DOOS" attach an -- true)"
|
||||
add doos_host_slirp "doos host mode" default doos "$(avg_ms "$DOOS" attach hs -- true)"
|
||||
add doos_alpine_slirp "doos Alpine image" default doos "$(avg_ms "$DOOS" attach as -- true)"
|
||||
add doos_alpine_pasta "doos Alpine, pasta" default doos "$(avg_ms "$DOOS" attach ap -- true)"
|
||||
if have podman; then
|
||||
add podman_none "Podman Alpine" none other "$(avg_ms podman run --rm --network none alpine true)"
|
||||
add podman_net "Podman Alpine" default other "$(avg_ms podman run --rm alpine true)"
|
||||
fi
|
||||
if have docker; then
|
||||
add docker_none "Docker Alpine (rootful)" none other "$(avg_ms docker run --rm --network none alpine true)"
|
||||
add docker_net "Docker Alpine (rootful)" default other "$(avg_ms docker run --rm alpine true)"
|
||||
fi
|
||||
|
||||
# ---- binary sizes
|
||||
size_of() { local p; p=$(command -v "$1" 2>/dev/null) && stat -L -c %s "$p" || echo null; }
|
||||
sizes=()
|
||||
for t in bwrap crun doos docker podman; do
|
||||
if [ "$t" = doos ]; then b=$(stat -c %s "$DOOS"); else b=$(size_of "$t"); fi
|
||||
sizes+=("{\"id\":\"$t\",\"bytes\":$b}")
|
||||
done
|
||||
|
||||
# ---- cpu limit: 4 busy loops for 4 seconds, CPU seconds consumed (user+sys)
|
||||
cpu_run() {
|
||||
/usr/bin/time -f "%U %S" "$DOOS" attach "$1" -- sh -c 'for i in 1 2 3 4; do (timeout 4 sh -c "while :; do :; done" &); done; sleep 4.2' 2>&1 >/dev/null | tail -1 | awk '{printf "%.2f", $1+$2}'
|
||||
}
|
||||
echo "bench: cpu limit" >&2
|
||||
"$DOOS" get_or_create hc >/dev/null 2>&1; "$DOOS" get_or_create --cpus 0.5 hc5 >/dev/null 2>&1
|
||||
cpu_free=$(cpu_run hc); cpu_half=$(cpu_run hc5)
|
||||
"$DOOS" delete --yes hc >/dev/null 2>&1; "$DOOS" delete --yes hc5 >/dev/null 2>&1
|
||||
|
||||
ver() { "$@" 2>/dev/null | head -1 | tr -d '"\\' ; }
|
||||
json_join() { local IFS=,; echo "$*"; }
|
||||
cat > "$OUT" <<JSON
|
||||
{
|
||||
"meta": {
|
||||
"date": "$(date +%F)",
|
||||
"kernel": "$(uname -r)",
|
||||
"cpus": $(nproc),
|
||||
"runs": $RUNS,
|
||||
"doos": "$(cat VERSION)",
|
||||
"podman": "$(have podman && ver podman --version || echo none)",
|
||||
"docker": "$(have docker && ver docker --version || echo none)",
|
||||
"bubblewrap": "$(have bwrap && ver bwrap --version || echo none)",
|
||||
"src_lines": $(cat src/*.cpp src/*.hpp | wc -l),
|
||||
"shared_libs": $(ldd "$DOOS" | wc -l)
|
||||
},
|
||||
"startup": [$(json_join "${rows[@]}")],
|
||||
"sizes": [$(json_join "${sizes[@]}")],
|
||||
"cpu": {"wall_seconds": 4.2, "busy_loops": 4, "unlimited": ${cpu_free:-null}, "half_cpu": ${cpu_half:-null}}
|
||||
}
|
||||
JSON
|
||||
echo "bench: wrote $OUT" >&2
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Checks the documentation set: every markdown file links to every other one, and every relative link resolves.
|
||||
# Usage: scripts/check-docs.sh (exit 1 on any problem, lists them all)
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DOCS=(README.md SPEC.md COMPARISON.md AGENTS.md CHANGELOG.md docs/ARCHITECTURE.md docs/RESEARCH.md)
|
||||
bad=0
|
||||
fail() { echo "docs: $*"; bad=$((bad+1)); }
|
||||
|
||||
# no markdown file may exist outside the checked set (so a new doc cannot dodge the rule)
|
||||
while IFS= read -r f; do
|
||||
f=${f#./}
|
||||
printf '%s\n' "${DOCS[@]}" | grep -qxF "$f" || fail "$f is not in the DOCS list of scripts/check-docs.sh"
|
||||
done < <(find . -name '*.md' -not -path './build/*' -not -path './.git/*' -not -path './prompt.md') # prompt.md: the original task statement, not documentation
|
||||
|
||||
for f in "${DOCS[@]}"; do
|
||||
[ -f "$f" ] || { fail "$f is missing"; continue; }
|
||||
dir=$(dirname "$f")
|
||||
# every other document must be linked
|
||||
for g in "${DOCS[@]}"; do
|
||||
[ "$f" = "$g" ] && continue
|
||||
rel=$(realpath --relative-to="$dir" "$g")
|
||||
grep -qE "\]\(${rel//./\\.}(#[^)]*)?\)" "$f" || fail "$f does not link to $g (expected ]($rel))"
|
||||
done
|
||||
# every relative link must point at something that exists
|
||||
while IFS= read -r target; do
|
||||
case "$target" in http://*|https://*|mailto:*|\#*) continue ;; esac
|
||||
path=${target%%#*}
|
||||
[ -z "$path" ] && continue
|
||||
[ -e "$dir/$path" ] || fail "$f links to missing $target"
|
||||
done < <(grep -oE '\]\([^)]+\)' "$f" | sed -E 's/^\]\(//; s/\)$//')
|
||||
done
|
||||
|
||||
[ "$bad" -eq 0 ] && echo "docs: ${#DOCS[@]} documents, all cross-linked, all links resolve" || { echo "docs: $bad problem(s)"; exit 1; }
|
||||
@@ -22,6 +22,8 @@ if grep -rl $'\xe2\x80\x94' --exclude-dir=build --exclude-dir=.git . | grep -q .
|
||||
echo "em dash found, remove it first" >&2; exit 1
|
||||
fi
|
||||
|
||||
python3 docs/charts/make_charts.py
|
||||
scripts/check-docs.sh
|
||||
echo "$new" > VERSION
|
||||
cmake -S . -B build >/dev/null
|
||||
cmake --build build
|
||||
|
||||
+10
-2
@@ -12,6 +12,7 @@ out() { "$DOOS" attach t -- sh -c "$1" 2>&1; }
|
||||
trap '"$DOOS" delete --yes t >/dev/null 2>&1; rm -rf "$DOOS_HOME"' EXIT
|
||||
|
||||
check "list empty" "$DOOS" list
|
||||
check "docs cross-linked, links resolve" "$(dirname "$0")/../scripts/check-docs.sh"
|
||||
check "get_or_create creates" "$DOOS" get_or_create t
|
||||
[ "$("$DOOS" get_or_create t)" = "exists t" ] && ok "get_or_create idempotent" || bad "get_or_create idempotent"
|
||||
check "invalid name rejected" bash -c "! '$DOOS' get_or_create 'Bad Name'"
|
||||
@@ -186,8 +187,15 @@ echo "--- network examples"
|
||||
curl -sS -m 2 -o /dev/null http://127.0.0.1:8077/ >/dev/null 2>&1 && bad "slirp: no inbound path" || ok "slirp: no inbound path"
|
||||
wait %% 2>/dev/null
|
||||
"$DOOS" get_or_create --net pasta pa1 >/dev/null 2>&1; "$DOOS" get_or_create --net pasta pa2 >/dev/null 2>&1
|
||||
"$DOOS" attach pa1 -- timeout 12 python3 -m http.server 8066 --bind 0.0.0.0 >/dev/null 2>&1 & sleep 5
|
||||
[ "$("$DOOS" attach pa2 -- curl -sS -m 2 -o /dev/null -w '%{http_code}' http://127.0.0.1:8066/ 2>/dev/null)" = "200" ] && ok "pasta: container to container via loopback" || bad "pasta: container to container via loopback"
|
||||
"$DOOS" attach pa1 -- timeout 25 python3 -m http.server 8066 --bind 0.0.0.0 >/dev/null 2>&1 &
|
||||
# pasta notices a newly bound port after a few seconds: poll instead of sleeping a fixed time
|
||||
c2c=000
|
||||
for _ in $(seq 1 18); do
|
||||
sleep 1
|
||||
c2c=$("$DOOS" attach pa2 -- curl -sS -m 2 -o /dev/null -w '%{http_code}' http://127.0.0.1:8066/ 2>/dev/null)
|
||||
[ "$c2c" = "200" ] && break
|
||||
done
|
||||
[ "$c2c" = "200" ] && ok "pasta: container to container via loopback" || bad "pasta: container to container via loopback"
|
||||
wait 2>/dev/null
|
||||
"$DOOS" delete --yes pa1 >/dev/null; "$DOOS" delete --yes pa2 >/dev/null; "$DOOS" delete --yes nets >/dev/null; "$DOOS" delete --yes iso >/dev/null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user