#!/usr/bin/env bash # Share ONE Docker daemon across WSL distros, instead of running one per distro. # # Why this exists # --------------- # WSL2 distros share a kernel and a network stack. Two dockerd instances then # contend over docker0 and iptables, which can disturb the daemon you actually # depend on. Docker Desktop avoids this by running a single daemon in a # dedicated distro and sharing its socket — this is the same idea, without # Docker Desktop. # # So a throwaway rig box does NOT install Docker. It borrows the daemon from # whichever distro is the designated host. That also makes the test more honest: # rig never installs Docker anyway — Docker is its documented prerequisite. # # How # --- # /mnt/wsl is a tmpfs with `shared` mount propagation, visible to every distro # in the WSL VM. The owning distro exposes its socket there; guests point # DOCKER_HOST at it. Two ways, with different costs: # # share bind-mount the existing socket onto the shared tmpfs. # Instant, and dockerd is NEVER restarted. Lasts until the # next WSL shutdown. # share --persist additionally install a systemd drop-in so dockerd listens # there itself. Survives restarts, but requires one Docker # restart now — which stops every container that has no # restart policy, since live-restore is off by default. # # The bind mount is the default precisely because the persistent version's cost # is paid on a machine that is already working. # # Reversibility is the whole design # --------------------------------- # `unshare` removes the bind mount (no restart) and, if present, the drop-in. # The original systemd unit is never edited — only an additive drop-in file is # ever created — so undoing is deletion, not repair. `status` always states # which of the three roles a distro is in, in those words. # # Nothing here runs automatically. It does nothing until invoked. # # Usage: # dockerhost.sh status # which distro owns Docker; what this one uses # dockerhost.sh share # share it (bind mount, no daemon restart) # dockerhost.sh share --persist # ...and survive WSL restarts (restarts Docker) # dockerhost.sh unshare # undo it; this distro owns its Docker again # dockerhost.sh use [--persist] # point THIS distro at the shared socket set -euo pipefail SHARED_DIR=/mnt/wsl/shared-docker SHARED_SOCK="$SHARED_DIR/docker.sock" OWNER_FILE="$SHARED_DIR/OWNER" DROPIN=/etc/systemd/system/docker.service.d/10-rig-shared-socket.conf PROFILE_D=/etc/profile.d/rig-docker-host.sh distro_name() { echo "${WSL_DISTRO_NAME:-$(hostname)}"; } require_wsl() { grep -qi microsoft /proc/version 2>/dev/null && return 0 echo "dockerhost is WSL-only: it relies on /mnt/wsl being shared between distros." >&2 exit 1 } # ── status ───────────────────────────────────────────────────────────────── status() { require_wsl echo "distro $(distro_name)" if [ -f "$DROPIN" ] || mountpoint -q "$SHARED_SOCK" 2>/dev/null; then echo "role SHARING — this distro's Docker is offered to other distros" elif [ -n "${DOCKER_HOST:-}" ] && [ "${DOCKER_HOST}" = "unix://$SHARED_SOCK" ]; then echo "role BORROWING — using another distro's Docker" else echo "role standalone — this WSL installation has the main host Docker" fi echo if [ -S "$SHARED_SOCK" ]; then echo "shared sock $SHARED_SOCK (present)" [ -f "$OWNER_FILE" ] && sed 's/^/ /' "$OWNER_FILE" else echo "shared sock none — no distro is sharing right now" fi echo echo "DOCKER_HOST ${DOCKER_HOST:-(unset — using /var/run/docker.sock)}" if command -v docker >/dev/null 2>&1; then echo "docker $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo unreachable)" else echo "docker cli not installed" fi } # ── share / unshare (run on the host distro) ─────────────────────────────── # Default: expose the EXISTING socket by bind-mounting it onto the shared tmpfs. # /mnt/wsl has `shared` propagation, so the mount is visible in other distros. # # The point of doing it this way is that dockerd is never restarted. Restarting # it stops every container that has no restart policy (live-restore is off by # default), which on a working machine means quietly killing whatever you had # running. Not a trade worth making just to expose a socket. # # Cost: a bind mount does not survive a WSL VM shutdown. `--persist` adds the # systemd drop-in as well, which does survive but needs that one restart. share_bind() { mkdir -p "$SHARED_DIR" chmod 0755 "$SHARED_DIR" if mountpoint -q "$SHARED_SOCK" 2>/dev/null; then echo "already bind-mounted at $SHARED_SOCK" else [ -S /var/run/docker.sock ] || { echo "no /var/run/docker.sock here" >&2; exit 1; } # The target must exist as a file for a bind mount onto it. [ -e "$SHARED_SOCK" ] || : > "$SHARED_SOCK" mount --bind /var/run/docker.sock "$SHARED_SOCK" echo "bind-mounted /var/run/docker.sock -> $SHARED_SOCK (no daemon restart)" fi cat > "$OWNER_FILE" <&2; exit 1; } share_bind if [ "${1:-}" != "--persist" ]; then echo echo "This lasts until the next WSL shutdown. To make it survive, re-run with" echo "--persist — but note that adds a systemd drop-in and RESTARTS Docker," echo "which stops any container that has no restart policy." return 0 fi if [ -f "$DROPIN" ]; then echo "drop-in already present — sharing persists across restarts." return 0 fi echo echo "--persist: installing a systemd drop-in and restarting Docker." echo "Containers without a restart policy will stop and will NOT come back." docker ps --format ' {{.Names}} restart={{.HostConfig.RestartPolicy.Name}}' 2>/dev/null \ || docker ps --format ' {{.Names}}' 2>/dev/null || true echo local exec_line exec_line=$(systemctl cat docker.service | grep -m1 '^ExecStart=') if [ -z "$exec_line" ]; then echo "could not read docker.service ExecStart — refusing to guess" >&2 exit 1 fi mkdir -p "$(dirname "$DROPIN")" "$SHARED_DIR" # Additive only: blank the inherited ExecStart, then restate it verbatim # with one extra -H. Nothing about the original unit is edited. cat > "$DROPIN" < "$OWNER_FILE" <&2; exit 1; } local did=0 # The bind mount first: undoing it needs no restart, so a plain `share` # is fully reversible without disturbing anything. if mountpoint -q "$SHARED_SOCK" 2>/dev/null; then umount "$SHARED_SOCK" rm -f "$SHARED_SOCK" echo " removed the bind mount (no restart needed)" did=1 fi rm -f "$OWNER_FILE" rmdir "$SHARED_DIR" 2>/dev/null || true if [ -f "$DROPIN" ]; then rm -f "$DROPIN" rmdir "$(dirname "$DROPIN")" 2>/dev/null || true systemctl daemon-reload systemctl restart docker echo " removed the systemd drop-in and restarted Docker" did=1 fi if [ "$did" -eq 0 ]; then echo "not sharing — this WSL installation already has the main host Docker." return 0 fi echo "restored: this WSL installation has the main host Docker again." echo " (nothing else was changed; the original unit was never edited)" } # ── use (run on a guest distro) ──────────────────────────────────────────── use() { require_wsl if [ ! -S "$SHARED_SOCK" ]; then echo "no shared socket at $SHARED_SOCK" >&2 echo "Run 'sudo bash ctrl/dockerhost.sh share' in the distro that owns Docker." >&2 exit 1 fi # Align the local docker group GID with the owner's, or the socket is # unreadable here even though it is visible. if [ -f "$OWNER_FILE" ] && [ "$(id -u)" -eq 0 ]; then local gid; gid=$(awk '/docker gid:/ {print $3}' "$OWNER_FILE") if [ -n "$gid" ]; then if getent group docker >/dev/null; then [ "$(getent group docker | cut -d: -f3)" = "$gid" ] || groupmod -g "$gid" docker else groupadd -g "$gid" docker fi fi fi if [ "${1:-}" = "--persist" ]; then [ "$(id -u)" -eq 0 ] || { echo "--persist needs root" >&2; exit 1; } echo "export DOCKER_HOST=unix://$SHARED_SOCK" > "$PROFILE_D" echo "persisted in $PROFILE_D" fi echo "export DOCKER_HOST=unix://$SHARED_SOCK" } case "${1:-status}" in status) status ;; share) shift; share "${1:-}" ;; unshare) unshare_ ;; use) shift; use "${1:-}" ;; *) echo "usage: $0 [status|share|unshare|use [--persist]]" >&2; exit 1 ;; esac