Files
doos/README.md
T
2026-10-23 23:26:57 +02:00

12 KiB

doos

Docs: README · SPEC · ARCHITECTURE · RESEARCH · COMPARISON · AGENTS · CHANGELOG

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
doos apparmor        # profile text for this binary (Ubuntu 24.04+)

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.

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+ (kernel.apparmor_restrict_unprivileged_userns=1) AppArmor must allow user namespaces for the binary. Generate a profile for the exact path doos is installed at, once:

doos apparmor | sudo tee /etc/apparmor.d/doos-local >/dev/null && sudo apparmor_parser -r /etc/apparmor.d/doos-local

If you skip this, doos tells you the exact command (with a file name unique to the binary's path, so installing doos in a second place does not replace the first profile). apparmor/doos is a static profile for packaging that covers /usr/local/bin, /usr/bin, /opt/doos/bin, ~/.local/bin, ~/bin and ~/projects/doos/build.

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 <hex> 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.

Networking

Every example below was run on the development machine. Pick the mode when you create the container; it cannot change later.

Mode Outbound Host 127.0.0.1 from inside Reach a port served inside, from the host Host abstract unix sockets
slirp (default) yes blocked no not shared
pasta yes reachable yes (forwarded automatically) not shared
host yes the same network yes shared
none no no no not shared
# slirp (default): the internet works, the host's loopback does not
doos attach web -- curl -sS -o /dev/null -w "%{http_code}\n" https://example.com          # 200
doos attach web -- curl -sS -m 3 http://127.0.0.1:8099                                      # fails: host loopback is blocked
doos attach web -- getent hosts example.com                                                 # DNS works (resolver 10.0.2.3)

# pasta: behaves like the host network, in a private namespace
doos attach --net pasta dev -- curl -sS http://127.0.0.1:8099                               # reaches a service on the host's loopback
doos attach dev -- python3 -m http.server 8088 --bind 0.0.0.0                               # then, on the host:  curl http://127.0.0.1:8088/
doos attach --image alpine --net pasta al -- wget -qO- https://example.com                  # works with images too

# none: no network at all, only loopback
doos attach --net none iso -- curl -m 3 https://example.com                                 # curl exits 6, /proc/net/dev lists only lo

# host: the host's own network namespace, explicit opt-in (prints a note recommending pasta)
doos attach --net host h -- curl -sS -o /dev/null -w "%{http_code}\n" https://example.com  # 200

Notes, all measured:

  • slirp has no inbound path: a server started in a slirp container is not reachable from the host (connection fails). Use pasta for that.
  • pasta notices a newly bound port after about 2 seconds, so start the server and give it a moment before connecting from the host.
  • Two pasta containers can talk to each other through the host's loopback: start a server in one (python3 -m http.server 8066 --bind 0.0.0.0), wait about 5 seconds, then doos attach cli -- curl http://127.0.0.1:8066/ from the other returns 200. The host's own public address is not forwarded; use 127.0.0.1 or the default gateway address.
  • pasta starts in about 65 ms per attach, slirp about 40 ms, none about 10 ms.

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/<pid>/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.
AppArmor restricts user namespaces for this binary Run the command doos prints (doos apparmor | sudo tee ... && sudo apparmor_parser -r ...), once per install path.
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.

Is it a security-wise Docker replacement? Same class as rootless Podman by design (no root daemon, user namespaces, seccomp, dropped capabilities, cgroup limits), but unaudited, tested on one machine, with a seccomp denylist and no per-container AppArmor/SELinux profile. Fine for your own tools; for untrusted code use rootless Podman, gVisor or Kata. Details: COMPARISON section 6.1.

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; the charts and how they are generated are in COMPARISON 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, remaining limits
COMPARISON.md doos next to Docker, Podman, bubblewrap, nspawn, LXC/Incus, Apptainer, Distrobox, gVisor/Kata, with pros, cons, measurements and a conclusion
AGENTS.md Rules for AI agents, release workflow
CHANGELOG.md Release history

Not sure whether doos is the right tool? Read the conclusion of COMPARISON first.