Skip to content

feat: opt-in rootless Docker + kind in the workspace - #847

Closed
ppat wants to merge 9 commits into
mainfrom
feat/rootless-docker-kind-workspace
Closed

feat: opt-in rootless Docker + kind in the workspace#847
ppat wants to merge 9 commits into
mainfrom
feat/rootless-docker-kind-workspace

Conversation

@ppat

@ppat ppat commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes #846.

What

Adds an opt-in enable_docker workspace parameter that runs a rootless Docker daemon inside the workspace, so Kubernetes/GitOps work — building images, running kind clusters for chainsaw tests — can happen in-workspace. The pod stays unprivileged: no privileged, no added capabilities, no host Docker socket mounted.

Full design + rejected alternatives: DESIGN-DIND.md.

How

  • Image (Dockerfile): rootless-docker plumbing baked into system-baseuidmap, slirp4netns, fuse-overlayfs, iptables, and docker-ce/cli/containerd/buildx from Docker's official apt repo (for the dockerd-rootless*.sh helpers). subuid/subgid guaranteed for coder before USER coder. kind is not baked in — it comes from dotfiles (aqua).
  • Template: enable_docker gates a dedicated docker-data PVC (survives stop/start — gated on enable_docker, deliberately not start_count), an XDG_RUNTIME_DIR emptyDir, DOCKER_HOST, a com.coder.workspace.docker-enabled marker label, and a rootless dockerd bring-up in the agent startup script (non-fatal, storage-driver auto-detect).
  • hostUsers: the pinned kubernetes provider (3.2.1) has no host_users field, so spec.hostUsers: false is injected by a Kyverno mutate policy keyed off the marker label. Companion PR: feat(apps-coder): Kyverno policy to set hostUsers on docker workspaces homelab-ops-kubernetes-apps#3351.

Testing

  • terraform validate/fmt/tflint, hadolint, shellcheck pass (pre-commit).
  • Userns path confirmed live in a workspace on the target cluster (kernel 6.11, cgroup v2, unshare -Ur OK, subuid/subgid present).
  • Companion apps PR's chainsaw CI asserts the policy mutates a marked pod and leaves unmarked pods untouched.
  • Full exercise is via the test_mode dry-run flow — see TESTING.md.

Merge ordering

Merge the companion apps PR (#3351) first so the Kyverno policy exists when a workspace opts in; without it the pod still runs, just in the weaker (no-hostUsers) posture.

ppat and others added 2 commits July 20, 2026 21:16
Adds an enable_docker workspace parameter that runs a rootless Docker daemon
inside the (still unprivileged, no-socket-mount) workspace pod, so k8s/gitops
work — building images, running kind clusters for chainsaw tests — can happen
in-workspace.

Image: rootless-docker plumbing baked into system-base (uidmap, slirp4netns,
fuse-overlayfs, iptables, docker-ce from Docker's apt repo for the
dockerd-rootless helpers); subuid/subgid guaranteed for coder. kind itself
comes from the operator's dotfiles (aqua), not the image.

Template: enable_docker gates a docker-data PVC (survives stop/start, not
start_count-gated), an XDG_RUNTIME_DIR emptyDir, DOCKER_HOST, and a gated
rootless dockerd bring-up in the agent startup script. The pod carries a
com.coder.workspace.docker-enabled marker label; a Kyverno policy (apps repo)
injects spec.hostUsers=false since the kubernetes provider 3.2.1 has no
host_users field yet.

Co-Authored-By: Claude <noreply@anthropic.com>
New DESIGN-DIND.md covers why the workspace runs Docker/kind while staying
unprivileged (rejected alternatives, the Kyverno-injected hostUsers indirection,
storage/driver trade-offs, and the cluster-migration angle). DESIGN.md, README.md,
CLAUDE.md, and TESTING.md gain brief pointers plus the enable_docker gotchas and
the live verification recipe.

Co-Authored-By: Claude <noreply@anthropic.com>
ppat added a commit to ppat/homelab-ops-kubernetes-apps that referenced this pull request Jul 20, 2026
#3351)

Coder workspaces that opt into rootless Docker (via the workspace template's
enable_docker parameter) need spec.hostUsers=false so the in-workspace
container engine runs in a private user namespace and cannot map to host UIDs.

The hashicorp/kubernetes Terraform provider (3.2.1) has no host_users field
yet, so the template cannot set it directly. This mutate ClusterPolicy injects
it at admission time, scoped by the docker-enabled marker label the template
stamps only when Docker is enabled.

failurePolicy is Ignore: a Kyverno outage must not block workspace creation.
Retire this policy once the provider gains host_users (draft PR #2914).

Pairs with the coder repo template change PR (ppat/coder#847) to implement ppat/coder#846.

Co-Authored-By: Claude <noreply@anthropic.com>
ppat and others added 7 commits July 20, 2026 22:03
The rootless dockerd daemon never started in a real workspace:
dockerd-rootless.sh was absent. Those entrypoints ship in the separate
docker-ce-rootless-extras package, which docker-ce does not pull in. Add it,
and have the startup script fail with a clear message (rather than a cryptic
setsid error) if the binary is ever missing again.

Co-Authored-By: Claude <noreply@anthropic.com>
Rootless dockerd failed with 'newuidmap: write to uid_map failed: Operation
not permitted' under hostUsers=false. The pod userns is only 65536 IDs wide
(0-65535), but useradd had set coder:165536:65536 -- pointing outside it. Our
previous append-if-missing never fired because useradd's entry was already
there. Overwrite with coder:10002:55534 (10002..65535, above coder's UID 10001),
which fits the namespace.

Also make the XDG_RUNTIME_DIR chmod non-fatal: the emptyDir is root-owned but
group-writable via fsGroup, so it is already usable and the chmod only produced
a spurious error.

Co-Authored-By: Claude <noreply@anthropic.com>
test_mode workspaces run a mutable branch tag (branch-<name>) that is rebuilt
in place. With the default IfNotPresent policy a node that cached the tag never
re-pulls, so template/image fixes silently test against a stale image. Set
imagePullPolicy=Always in test_mode; released (immutable) tags keep IfNotPresent.

Co-Authored-By: Claude <noreply@anthropic.com>
Rootless-in-userns hit an unavoidable wall: allowPrivilegeEscalation=false sets
no_new_privs, which neuters setuid newuidmap, so rootlesskit cannot set up its
multi-UID map (EPERM) even with an in-range subuid. hostUsers=false already gives
us safe namespaced root (root in the pod maps to an unprivileged host uid), so
run the container as root and use a plain rootful dockerd instead -- no newuidmap,
no subuid, no rootless plumbing.

- New script-workspace-entrypoint.sh (runs as root): does the apt/homebrew prep
  (absorbed from the old init container), starts dockerd (non-fatal), then drops
  to the coder user via setpriv and execs the coder agent. The coder user keeps
  its 10001 identity and role.
- deployment.tf: drop the init container and the whole system-emptyDir + /usr
  /etc /var subPath dance (only existed to move root-made changes into a non-root
  container). Main container runs as uid 0 with CAP_SYS_ADMIN,NET_ADMIN and
  allowPrivilegeEscalation=true; privileged stays false. Safe because the platform
  Kyverno policy sets hostUsers:false via the always-present docker-enabled marker.
- Docker data PVC now backs /var/lib/docker (rootful), created unconditionally so
  it survives stop.
- Drop the enable_docker parameter (docker is always on) and the DOCKER_HOST env
  (rootful uses the default socket; coder reaches it via the docker group).
- script-agent-startup.sh reverts to a coder-user stub.

Image still carries the (now-unused) rootless packages; cleanup deferred until
this is confirmed working.

Co-Authored-By: Claude <noreply@anthropic.com>
dockerd failed to extract image layers: 'mount ... bind ... permission denied',
with 'could not setup daemon root propagation to shared' earlier. Root cause: the
/var/lib/docker PVC is mounted by the kubelet OUTSIDE the pod user namespace, so it
is a locked private mount -- dockerd cannot make it shared or bind-mount snapshots
within it, even as namespaced root with CAP_SYS_ADMIN (verified: findmnt shows
PROPAGATION=private, mount --make-rshared -> EPERM).

Fix: drop the /var/lib/docker PVC entirely. The entrypoint instead bind-mounts a
dir under the home PVC (/home/coder/.var/docker) onto /var/lib/docker inside this
container's own mount namespace -- an unlocked mount dockerd CAN manage -- and
makes it shared. Data still persists across stop via the home PVC. Both dirs are
set root:root 0711 (dockerd rejects a group/world-accessible data root; the home
PVC is otherwise fsGroup-owned by the coder group).

Co-Authored-By: Claude <noreply@anthropic.com>
The entrypoint's in-container bind mount was silently blocked by the container
AppArmor profile (mount is denied there), so it never took effect. Mount the docker
data root as a kubelet-provided emptyDir instead: it is a plain non-overlay fs set
up outside the container, avoiding both overlay-on-overlay (the container rootfs is
overlayfs) and the in-container mount restriction. Entrypoint just sets ownership
(root:root 0711); no in-container mount needed.

Tradeoff: emptyDir does not persist across workspace stop -- revisit persistence
once the runtime is confirmed working end to end.

Co-Authored-By: Claude <noreply@anthropic.com>
emptyDir at /var/lib/docker gives containerd an ext4 (non-overlay) data root, yet
image extraction still fails 'permission denied' on containerd's internal bind mount
-- proving it is NOT overlay-on-overlay but the cri-containerd AppArmor profile's
deny-mount rule. privileged=true sets the profile to unconfined; if docker then works,
the durable fix is appArmorProfile:Unconfined via Kyverno (provider has no field for
it), without going fully privileged.

Testing step, not the final posture.

Co-Authored-By: Claude <noreply@anthropic.com>
@ppat ppat closed this Jul 21, 2026
@ppat
ppat deleted the feat/rootless-docker-kind-workspace branch July 21, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: opt-in rootless Docker + kind in the workspace (unprivileged)

1 participant