# Docker inside a smolvm microVM # # Runs Docker Engine in a bare Alpine VM. Init commands run directly in the VM # as root via VmExec, so Docker gets full kernel privileges without any # capability restrictions. # # This also works with `--image`: a privileged (default) image-based machine # runs in an OCI container, but /storage is bind-mounted into it, so the same # `mount --bind /storage/docker /var/lib/docker` resolves to the ext4 disk just # as in a bare VM. # # ── Kernel requirements ─────────────────────────────────────────────────────── # # Requires libkrunfw built from config-libkrunfw_aarch64_lean (or later) with: # CONFIG_OVERLAY_FS=y — overlayfs (rootfs overlay) # CONFIG_NETFILTER=y — iptables/nftables for Docker networking # CONFIG_NF_TABLES=y — nftables netlink (Alpine's iptables-nft) # CONFIG_NFT_COMPAT=y — iptables compatibility via nftables # CONFIG_BRIDGE=y — docker0 bridge interface # # If the current libkrunfw.5.dylib lacks these, see docs/building-libkrunfw-macos.md. # # ── Storage driver note ─────────────────────────────────────────────────────── # # Docker's /var/lib/docker MUST be on ext4 (/dev/vda), not on the rootfs # overlayfs. This is a hard requirement, not a workaround. # # Why: the smolvm rootfs overlay uses the initramfs (ramfs) as its lower # layer. Ramfs has no file-handle support (no exportfs). This propagates up # through the overlayfs — any directory ON the rootfs overlay (like # /var/lib/docker) also lacks file-handle support. The kernel then falls # back to index=off and rejects it as an upper dir for Docker's nested # overlay, regardless of CONFIG_OVERLAY_FS_REDIRECT_DIR or index=on mount # options. # # Fix: bind-mount /storage/docker → /var/lib/docker before starting dockerd. # /storage/ is mounted from /dev/vda (ext4), which supports file handles. # Docker's overlay2 layers then live on ext4 and nest correctly. # # This bind-mount must be re-applied after every VM start (mounts don't # persist across stop/start). The init commands create the directory on # first start; the "Start dockerd" exec command does the bind-mount each # time. # # ── Create and start ───────────────────────────────────────────────────────── # # smolvm machine create --name docker -s examples/docker-in-vm/docker.smolfile --net-backend virtio-net # smolvm machine start --name docker # # ── Start dockerd ───────────────────────────────────────────────────────────── # # smolvm machine exec --name docker -- sh -c ' # mkdir -p /storage/docker /var/lib/docker # mount --bind /storage/docker /var/lib/docker # rm -f /var/run/docker.pid # dockerd --storage-driver=overlay2 >/tmp/dockerd.log 2>&1 & # for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 1; done # docker info | grep "Storage Driver"' # # ── Run containers ──────────────────────────────────────────────────────────── # # # Pull an image # smolvm machine exec --name docker -- docker pull alpine # # # Run with bridge networking (default — requires netfilter kernel config) # smolvm machine exec --name docker -- docker run --rm alpine echo "hello from docker" # # # Run with host networking (works even without netfilter) # smolvm machine exec --name docker -- docker run --rm --network=host alpine wget -qO- https://example.com # # # Build an image # smolvm machine exec --name docker -- sh -c ' # printf "FROM alpine\nRUN echo built" > /tmp/Dockerfile # docker build -t myapp /tmp' # # ── Interactive shell inside a Docker container ─────────────────────────────── # # smolvm machine exec -i --name docker -- docker run --rm -i alpine sh # # ── Kubernetes (K3d / kind) ─────────────────────────────────────────────────── # # The guest kernel + this setup run k3s. The engine now creates /dev/kmsg in # the container /dev (the kubelet hard-requires it), so K3d works with no extra # prep beyond a running dockerd: # # smolvm machine exec --name docker -- sh -c ' # curl -sL https://github.com/k3d-io/k3d/releases/latest/download/k3d-linux-$(uname -m | sed s/aarch64/arm64/;s/x86_64/amd64/) -o /usr/local/bin/k3d # chmod +x /usr/local/bin/k3d # k3d cluster create dev --no-lb # kubectl get nodes' # -> node Ready # # ── Cloud (smolmachines) note ───────────────────────────────────────────────── # # On the cloud platform each `exec` runs in its own mount namespace, so the # `mount --bind` above is invisible to dockerd. Point docker's data-root # straight at the ext4 disk instead: # # dockerd --data-root=/storage/docker --storage-driver=overlay2 # # ── Stop ───────────────────────────────────────────────────────────────────── # # smolvm machine stop --name docker # # ───────────────────────────────────────────────────────────────────────────── cpus = 2 memory = 2048 net = true storage = 20 init = [ "apk update -q", # docker: daemon + CLI "apk add docker -q", # Bind /var/lib/docker to ext4 storage so overlay2 works (avoids overlay-on-overlay) "mkdir -p /storage/docker /var/lib/docker", "mount --bind /storage/docker /var/lib/docker", ]