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.
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), `unzip` and `fusermount3` (or `fusermount`; only for `--mount`/`--davmount` against a `dav://`/`davs://`/`http(s)://` source). On Ubuntu 24.04+ (`kernel.apparmor_restrict_unprivileged_userns=1`) AppArmor must allow user namespaces for the
| 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`.
-`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.
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`.
`--mount` gives a container extra storage without counting against `--size`. For a plain host directory: `doos attach --mount /some/host/dir:/data NAME` (`SRC` must exist and be a directory when the container starts; add `:ro` for read-only).
For remote storage, give a WebDAV URL instead of a host path and doos does the rest: it downloads, verifies and installs `rclone` once into its own managed directory under `$DOOS_HOME` (never the system, never your `PATH`), then drives it to mount the share on the host before the container starts:
```
doos attach --mount davs://user:pass@cloud.example.com/remote.php/webdav /data NAME
# or, keeping the password out of the URL and shell history:
doos attach --davmount https://cloud.example.com/remote.php/webdav /data --dav-user user --dav-pass pass --dav-vendor nextcloud NAME
```
`dav://` and `davs://` are shorthand for plain-http and https WebDAV; a bare `http://`/`https://` URL works the same way. The mount is deduplicated by target (URL + credentials + vendor) and reused across containers and attaches instead of remounting every time; it lives on the host, outside any container, and keeps running after the container exits (writes go through a short write-back cache, about a second, before they reach the server). See `doos davmount list` and `doos davmount umount [--yes] ID` to inspect or tear one down, and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for how it is verified and supervised.
- **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.
| `AppArmor restricts user namespaces for this binary` | Run the command doos prints (`doos apparmor \| sudo tee ... && sudo apparmor_parser -r ...`), once per install path. |
| `davmount failed for URL: authentication failed` | Wrong `--dav-user`/`--dav-pass` (or credentials embedded in the URL); fix and retry. |
| `davmount failed for URL: cannot reach the server` | Check the URL, the server, and network/DNS from the host. |
| `davmount failed for URL: TLS certificate error` | Self-signed server: pass `--dav-insecure` if you trust it. |
| `davmount failed for URL: the server does not look like a WebDAV endpoint` | Wrong path or `--dav-vendor`; check the URL against the server's WebDAV docs. |
| `davmount needs 'unzip' installed` | Install `unzip` (only needed the first time rclone is installed). |
`make help` lists the targets: `make build`, `make test` (end-to-end, needs network), `make audit` (named CVE regression checks, see [audit/README.md](audit/README.md)), `make valgrind` (memcheck against a fixed set of invocations, see [valgrind/README.md](valgrind/README.md)), `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/`. CI (`.gitea/workflows/ci.yml`) runs `build`, `valgrind`, `test` and `audit` on every push; see [docs/CI.md](docs/CI.md) for what each needs and the one-time runner setup.
| [`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 |