Files
retoorandClaude Sonnet 5 4c754b20da
ci / build (push) Successful in 54s
ci / valgrind (push) Successful in 1m42s
ci / test (push) Canceled after 0s
ci / audit (push) Canceled after 0s
Add Gitea CI, fix size overflow and helper-path inconsistency from review2.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-24 01:20:18 +02:00

187 lines
13 KiB
Markdown

# 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) · [CI](docs/CI.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.
```
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`. Release history: [CHANGELOG.md](CHANGELOG.md). License: [MIT](LICENSE).
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 |
| `--i-understand` | required on every call that sets or uses `--hardening relaxed` or `--net host` | absent | 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](COMPARISON.md) section 6.1.
## Development
`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.
## Project docs
| File | Content |
|---|---|
| [`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 |
| [`audit/README.md`](audit/README.md) | Named CVE regression checks run against `build/doos` (`make audit`) |
| [`valgrind/README.md`](valgrind/README.md) | Memcheck against a fixed set of invocations (`make valgrind`) |
| [`docs/CI.md`](docs/CI.md) | Gitea Actions: what runs where, runner setup |
Not sure whether doos is the right tool? Read the conclusion of [COMPARISON](COMPARISON.md) first.