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