# doos 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. ``` doos list doos get_or_create [OPTIONS] NAME doos attach [OPTIONS] NAME [-- CMD...] doos delete [--yes] NAME doos image list | add [--sha256 HEX] SOURCE [NAME] | delete [--yes] NAME ``` Version: see `VERSION` (currently 1.1.0). 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. ``` doos list doos get_or_create [--net slirp|host|none] NAME doos attach [--net slirp|host|none] NAME [-- CMD...] doos delete [--yes] NAME ``` Build: `cmake -S . -B build && cmake --build build`. Test: `tests/run.sh`. Requirements: Linux >= 5.11, `slirp4netns` (default network mode), `pasta` (only for `--net pasta`), `gnupg` (signature checks; optional), `uidmap` (`newuidmap`/`newgidmap`, for more than one id inside; falls back to one id), `curl`, `tar` and `sha256sum` (images). On Ubuntu 24.04+ install the AppArmor profile once: ``` sudo cp apparmor/doos /etc/apparmor.d/doos && sudo apparmor_parser -r /etc/apparmor.d/doos ``` See `SPEC.md` (behavior, limits) and `docs/RESEARCH.md` (what works, measurements, known gaps). ## Usage examples ``` doos attach dev # host mode: your system's /usr as base, shell as root inside doos attach --image alpine box # first use downloads and verifies Alpine, then opens a shell doos attach --image ubuntu web -- apt-get update doos attach --image ubuntu-24.04 old # a specific Ubuntu release doos attach --size 8g big # raise the writable-layer cap (default 4g) doos attach --net none sandbox -- sh # no network at all doos list doos image list # what is installed, what the catalog offers doos delete dev # dry run: shows what would go doos delete --yes dev # really deletes doos image delete --yes alpine # refused while a container uses it ``` Your own image (a root filesystem tarball): ``` doos image add https://example.org/debian-rootfs.tar.xz --sha256 debian # URL needs the hash doos image add ./rootfs.tar.gz mybase # local file doos attach --image mybase c1 ``` ## Host mode or image mode | | Host mode (default) | Image mode (`--image`) | |---|---|---| | Base system | your `/usr`, `/opt`, copy of `/etc` | the image, extracted once and shared | | Needs a download | no | first use only | | `apt-get install` | works when the host is Debian/Ubuntu based | works for the image's own package manager (apt, apk) | | Sees host files | `/usr`, `/opt`, readable `/etc` at creation | nothing from the host | | Best for | quick throwaway shells with your tools | reproducible, separate systems | Writes always stay in the container (copy-on-write); `/root` and `/var` persist, `/tmp` is memory-backed and lost on exit. A container is `running` while its first `attach` command is alive; more `attach` calls join it. Data persists until `delete --yes`. ## Options | Option | Meaning | Default | Later | |---|---|---|---| | `--image NAME` | alpine, ubuntu, ubuntu-24.04, debian, debian-bookworm, or your own | host system | fixed | | `--net M` | `slirp` (outbound only, host loopback blocked), `pasta` (host-like, private sockets), `host`, `none` | slirp | fixed | | `--ids X` | `single` or `subuid` | subuid if possible | fixed | | `--hardening H` | `default` or `relaxed` | default | fixed | | `--size S` | writable data cap | 4g | changeable | | `--memory S` | memory cap | 8g | changeable | | `--pids N` | process/thread cap | 4096 | changeable | | `--cpus N` | cpu cap, e.g. `0.5`, `2` | all cores | changeable | Changeable options apply the next time the container starts. Examples: `doos attach --memory 2g --cpus 1 build`, `doos attach --net pasta --image debian-bookworm web`. ## Limits, size cap and hardening - **Limits** (`--memory`, `--pids`, `--cpus`): cgroup v2, measured here: a 256m container touching 400 MB is killed whole and doos says so; 80 forks against `--pids 32` fail and are reported; `--cpus 0.5` held 4 busy loops to 2.3 CPU-seconds in 4.3 s of wall time. - **Size cap**: `--size` limits what a container writes. Checked from inside the id map (so other users' files count), warns at 80%, freezes at 100% (nothing is deleted), thaws below 95%. A second `doos attach NAME -- ...` is never frozen, so you can clean up. No single file can exceed the cap. - **Hardening** (default): capabilities reduced to Docker's default set, a seccomp filter (no mounts, keyrings, bpf, perf, io_uring, nested user namespaces, kexec, module loading...), and a host `/etc` allowlist instead of a full copy. `--hardening relaxed` switches all three off for tools that need them (container runtimes, Chromium's sandbox). - All limits need cgroup v2 delegation (standard on systemd desktops). Without it doos prints that nothing is enforced. ## Configuration | Variable | Meaning | |---|---| | `DOOS_HOME` | State directory (default `~/.local/share/doos`). No `,` or `:` allowed. | | `XDG_DATA_HOME` | Used as `$XDG_DATA_HOME/doos` when `DOOS_HOME` is unset. | ## Troubleshooting | Message | Cause and fix | |---|---| | `write /proc//uid_map ... blocked` or `mount ...: Permission denied` | AppArmor restricts user namespaces. Install `apparmor/doos` (command above). | | `slirp4netns failed to start` | Install `slirp4netns`, or use `--net none` / `--net host`. | | `subuid mapping unavailable` | Install `uidmap` and make sure `/etc/subuid` and `/etc/subgid` list your user, or use `--ids single`. | | `sha256 mismatch` | The download is not what the publisher lists; nothing was installed. Retry or check the source. | | `a URL image needs --sha256` | Unverifiable input is rejected by design; pass the hash from the publisher. | | `limits are not enforced and the size cap only warns` | No delegated cgroup v2 (no systemd user manager). Limits and cap are advisory. | | `was killed by its memory limit` | Raise `--memory`. `/tmp` is memory-backed and counts. | | `hit its process limit` | Raise `--pids`; threads count as processes. | | `Operation not permitted` from `mount`, `unshare`, `keyctl`, io_uring | Hardening blocked it. Use `--hardening relaxed` on that container. | | `signature NOT checked (gpg is not installed)` | Install `gnupg` for signature checks; the checksum still applies. | | `doos: seeded ...: unreadable skipped: N` | Host entries the user cannot read were left out of the container. Informational. | | `state path must not contain ',' or ':'` | Set `DOOS_HOME` to a plain path. | | `container 'x' is running` on delete | Exit the attached command first. | | `container limit reached (64)` | Delete one first. | ## Safety summary Host files are never written: system dirs are reached only through overlayfs. Host `/home`, `/sys` and host loopback are not reachable in the default network mode. Images are verified before they are extracted: sha256 from the publisher, and for Alpine and Ubuntu a signature checked against a key pinned by fingerprint. Processes run with Docker's default capability set and a seccomp filter. Memory, pids, cpus and written data are capped. `--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`. ## 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 |