Add docker-rootless-cgroup module for cgroup v2 delegation

Rootless Docker only gets cgroup v2 controllers delegated when systemd
sets Delegate=yes on the user session. Kasm images have no systemd, so
the daemon comes up with CgroupDriver=none and `kind create cluster`
fails. This module reproduces that delegation by hand: evacuates the
cgroup-namespace root, delegates controllers into a user-owned /rootless
slice, installs a PATH shim that gives the daemon a private cgroup leaf
so it can self-evacuate, and restarts it. Guarded to no-op on systemd,
rootful, and non-Docker hosts; wired into bootstrap.sh last.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 11:53:46 -04:00
co-authored by Claude Opus 4.8
parent 39f638b8db
commit eff01b1b0b
4 changed files with 175 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env sh
# docker-rootless-cgroup/install.sh — give rootless Docker cgroup v2 controller
# delegation on a systemd-less host (e.g. Kasm), so per-container cgroups work
# and `kind` can create clusters.
#
# Why: rootless Docker only receives delegated cgroup v2 controllers when
# systemd sets `Delegate=yes` on the user session. Kasm images run no systemd
# (PID 1 is a VNC startup script), so the daemon comes up with CgroupDriver=none
# and `kind create cluster` fails with:
# "running kind with rootless provider requires setting systemd property
# Delegate=yes"
# The suggested fix can't be applied — there is no systemd unit to set it on.
#
# What this does, reproducing that delegation by hand (needs passwordless sudo
# for the host cgroup writes):
# 1. Evacuate the cgroup-namespace root into an /init leaf, then enable the
# controllers in the root's subtree_control. cgroup v2's "no internal
# processes" rule forbids enabling controllers while a cgroup still holds
# processes, so the evacuation has to come first. (In a container,
# /sys/fs/cgroup is the namespace root but a real nested cgroup on the
# host, so it is subject to that rule; only the true system root is exempt.)
# 2. Create a user-owned /rootless slice with the controllers delegated down.
# 3. Install a PATH shim at ~/.local/bin/dockerd-rootless.sh that drops the
# daemon into a PRIVATE leaf (/rootless/daemon) before exec'ing the real
# launcher. Rootless dockerd enables cgroups by self-evacuating (moving its
# own process into a child cgroup, then enabling subtree_control); it can
# only do that in a cgroup it does not share with foreign processes. The
# Kasm supervisor forks the daemon and so shares its launch cgroup, which
# blocks self-evacuation — hence the private leaf.
# 4. Move the daemon's supervisor into the /rootless subtree and restart the
# daemon so it comes back up through the shim, in the private leaf, and
# flips its CgroupDriver from none to cgroupfs.
#
# The cgroup layout is host state a Kasm redeploy wipes, so this is meant to be
# re-run at each bootstrap; the shim lives in $HOME and survives. It no-ops
# (Docker untouched) unless it finds rootless Docker on a cgroup v2 host with
# delegation actually missing — safe on systemd hosts and non-Docker hosts.
set -eu
say() { printf '%s\n' "$*" >&2; }
CG=/sys/fs/cgroup
SLICE="$CG/rootless"
BIN_DIR="$HOME/.local/bin"
SHIM="$BIN_DIR/dockerd-rootless.sh"
CONTROLLERS='+cpu +cpuset +io +memory +pids'
NEED='cpu cpuset io memory pids'
# --- guards: act only where this is both needed and possible ----------------
# Unified cgroup v2?
if [ ! -e "$CG/cgroup.controllers" ]; then
say "docker-rootless-cgroup: not a unified cgroup v2 host; skipping."
exit 0
fi
if ! command -v docker >/dev/null 2>&1; then
say "docker-rootless-cgroup: docker not installed; skipping."
exit 0
fi
# Rootless daemon? (systemd/rootful hosts manage their own cgroups.)
case "$(docker info -f '{{.SecurityOptions}}' 2>/dev/null || true)" in
*rootless*) : ;;
*) say "docker-rootless-cgroup: Docker is not rootless; the host handles cgroups. Skipping."
exit 0 ;;
esac
# Already delegated? Then there is nothing to do.
driver="$(docker info -f '{{.CgroupDriver}}' 2>/dev/null || true)"
if [ "$driver" != "none" ] && [ -n "$driver" ]; then
say "docker-rootless-cgroup: CgroupDriver=$driver already; delegation present. Nothing to do."
exit 0
fi
# The host must actually have delegated the controllers to this container; if
# it withheld them there is nothing we can do from inside.
avail=" $(cat "$CG/cgroup.controllers") "
for c in $NEED; do
case "$avail" in
*" $c "*) : ;;
*) say "docker-rootless-cgroup: host did not delegate '$c' to this container (have:$avail); cannot proceed. Skipping."
exit 0 ;;
esac
done
# Locate the running rootless daemon and its supervisor (its parent process,
# e.g. Kasm's custom_startup.sh loop that relaunches dockerd when it dies).
rk="$(pgrep -x rootlesskit | head -1 || true)"
if [ -z "$rk" ]; then
say "docker-rootless-cgroup: rootlesskit not running; is dockerd-rootless up? Skipping."
exit 0
fi
sup="$(ps -o ppid= -p "$rk" 2>/dev/null | tr -d ' ' || true)"
say "docker-rootless-cgroup: rootless Docker has CgroupDriver=none; delegating cgroup v2 controllers..."
# --- 1. evacuate the ns-root and delegate its controllers -------------------
sudo mkdir -p "$CG/init"
# Snapshot the pid list first (moving processes mutates cgroup.procs as we go).
for p in $(cat "$CG/cgroup.procs"); do
echo "$p" | sudo tee "$CG/init/cgroup.procs" >/dev/null 2>&1 || true
done
printf '%s' "$CONTROLLERS" | sudo tee "$CG/cgroup.subtree_control" >/dev/null
say " delegated controllers at ns-root: $(cat "$CG/cgroup.subtree_control")"
# --- 2. user-owned /rootless slice with controllers, private leaves ---------
sudo mkdir -p "$SLICE/init" "$SLICE/daemon"
printf '%s' "$CONTROLLERS" | sudo tee "$SLICE/cgroup.subtree_control" >/dev/null
sudo chown -R "$(id -u):$(id -g)" "$SLICE"
say " created delegated slice $SLICE (controllers: $(cat "$SLICE/cgroup.controllers"))"
# --- 3. PATH shim that gives the daemon a private cgroup leaf ----------------
mkdir -p "$BIN_DIR"
tmp="$(mktemp)"
cat > "$tmp" <<'SH'
#!/bin/sh
# Managed by dotfiles (docker-rootless-cgroup). Shadows /usr/bin/dockerd-rootless.sh
# on PATH. Places the rootless Docker daemon in a private, delegated cgroup v2
# leaf so it can enable controllers (needed by `kind`), then execs the real
# launcher. No-op if the delegated slice is absent, so it is safe anywhere.
_cg=/sys/fs/cgroup/rootless/daemon
if [ -w /sys/fs/cgroup/rootless ] && [ -d "$_cg" ]; then
echo $$ > "$_cg/cgroup.procs" 2>/dev/null || true
fi
exec /usr/bin/dockerd-rootless.sh "$@"
SH
chmod +x "$tmp"
if [ -f "$SHIM" ] && cmp -s "$tmp" "$SHIM"; then
rm -f "$tmp"
say " daemon cgroup shim already current at $SHIM"
else
mv "$tmp" "$SHIM"
chmod +x "$SHIM"
say " installed daemon cgroup shim at $SHIM"
fi
# --- 4. relocate the supervisor and restart the daemon ----------------------
# Put the supervisor inside the /rootless subtree so the daemon it relaunches
# (via the shim) starts there too — then the shim's move into /rootless/daemon
# needs no privilege (the common ancestor /rootless is user-owned).
if [ -n "$sup" ]; then
echo "$sup" | sudo tee "$SLICE/init/cgroup.procs" >/dev/null 2>&1 || true
fi
say " restarting rootless Docker so it comes back up in the private leaf..."
kill "$rk" 2>/dev/null || true
i=0
while [ "$i" -lt 30 ]; do
if docker info >/dev/null 2>&1; then break; fi
i=$((i + 1))
sleep 1
done
# --- verify: report honestly, do not claim success on none ------------------
driver="$(docker info -f '{{.CgroupDriver}}' 2>/dev/null || true)"
if [ "$driver" = "none" ] || [ -z "$driver" ]; then
say "docker-rootless-cgroup: WARNING — CgroupDriver is still '${driver:-unreachable}'."
say " kind will not work yet. Inspect the new dockerd's cgroup:"
say " cat /proc/\$(pgrep -x dockerd | head -1)/cgroup # expect .../rootless/daemon"
exit 0
fi
say "docker-rootless-cgroup: done. CgroupDriver=$driver — rootless Docker now has"
say " delegated cgroups; \`kind create cluster\` can run."