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

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.
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
S
Description
User-level containers in C++20: overlayfs, user namespaces, slirp4netns/pasta networking, cgroup v2 limits, seccomp hardening. No root at runtime.
Readme MIT
228 KiB
Languages
C++ 72.4%
Shell 24.5%
Makefile 2.7%
CMake 0.4%