docker run, docker build and docker compose can work inside the box,
without the host's Docker socket and without a privileged container. What
answers is podman running rootless as your user. It is
off by default, because nesting it forces the box's own isolation open: the
seccomp profile, the read-only /proc/sys and AppArmor all have to be lifted
for the container, which makes a container-to-host escape easier than it is
otherwise.
On the host:
- uncomment the podman block (
/dev/fuseand the threesecurity_opt) incompose.override.example.yaml, copied tocompose.override.yaml; - set
PODMAN_ENABLE=truein.env; - restart the container:
docker compose up -d.
The setup script does steps 1 and 2 for you when you answer yes to its
podman question, unless a compose.override.yaml already exists (then add
the block by hand). Why each line of the block is needed is
written in compose.override.example.yaml: /dev/fuse for fuse-overlayfs,
seccomp because the default profile refuses clone(CLONE_NEWUSER),
systempaths because a read-only /proc/sys stops crun and netavark, and
apparmor where AppArmor is active (Debian, Raspberry Pi OS). No
--privileged and no CAP_SYS_ADMIN.
Then:
podman-dockerprovides/usr/bin/dockeras a shim over thepodmanCLI;- the entrypoint starts
podman system serviceas your user on/run/user/1000/podman/podman.sock, and login shells exportDOCKER_HOST=unix:///run/user/1000/podman/podman.sock, so Compose v2 (thedocker-composepackage, a real Docker plugin) and anything else that talks to the socket finds it; - images live in
~/.local/share/containers, in the persistent home. They are outside the backup, so they re-pull.
docker run --rm alpine echo ok
docker build -t mine .
docker compose up -d && docker compose psWith PODMAN_ENABLE=false no socket is started and DOCKER_HOST is not set.
docker and podman then go through a wrapper that stops with the three steps
above instead of an obscure error:
The same wrapper points to ~/.cache/dev-box-podman.log when podman is enabled
but the socket never came up. PODMAN_FORCE=1 docker ..., or
/usr/bin/podman, bypasses it.
- Containers started here are rootless. There is no
--privilegedinside the box, publishing a port below 1024 is refused, since it would neednet.ipv4.ip_unprivileged_port_startlowered, and UIDs are mapped: a file written as root in a container belongs to100000on the host side of the bind mount. - Networking goes through pasta/slirp4netns rather than a host bridge.
Published ports are reachable from inside the box (
curl localhost:8080). Reaching them from your laptop means going through the box's own address (Tailscale). - Storage uses
fuse-overlayfs, since overlayfs cannot always stack on the overlay the box itself runs on. It is correct everywhere, and slower than native overlay on heavy I/O. - Docker on the host still owns the box itself. Restarting or rebuilding the container happens on the host, not in here.
- On a Raspberry Pi, rootless podman has not been confirmed on real hardware yet (see manual-install.md).
With podman on, devbox dbs starts development databases in it, see
databases.md, and devbox tui lazydocker becomes useful.
