Fix Docker builds under rootless daemons and expose git metadata to build containers

Two fixes from the vm4006 rollout (docs/plan/16-git-in-docker-builds.md):

Rootless daemons map the host user to container root, so running the build
container as --user <host-uid> put it into the subuid range and it could not
even create .gradle in a fresh worktree (legacy only worked because its
ownership-repair chown had accidentally moved build/ and .gradle/ into subuid
ownership in its reused primary checkout). The container now always runs as
--user 0: the unprivileged host user under rootless, real root under rootful
where the ownership repair still applies; under rootless it degenerates to 0:0.

Git now works inside build containers: the primary .git is mounted read-only
with .git/gittally/ masked by an empty tmpfs (git.token and the control token
stay unreachable, the workspace bind resurfaces only the build's own worktree)
and the worktree admin dir mounted read-write for index-refreshing commands.
Verified on vm4006: git log/status succeed, the machine config is invisible,
ref writes fail on the read-only mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-10 15:34:39 +02:00
co-authored by Claude Fable 5
parent 800fcae2f7
commit 8b71d0db8e
6 changed files with 151 additions and 11 deletions
@@ -126,7 +126,7 @@ class DockerBuildRunnerTest : FunSpec() {
script shouldContain "exit \$build_exit"
}
test("a rootless socket runs the container as the host user without group-add") {
test("a rootless socket runs the container as root (the host user) without group-add or host-id chown") {
every { socketLocator.locate("1000") } returns
DockerSocket(Paths.get("/run/user/1000/docker.sock"), rootless = true, gid = 998L)
@@ -134,8 +134,42 @@ class DockerBuildRunnerTest : FunSpec() {
val args = captured.single()
args shouldContain "/run/user/1000/docker.sock:/var/run/docker.sock"
args[args.indexOf("--user") + 1] shouldBe "1000"
args[args.indexOf("--user") + 1] shouldBe "0"
args shouldNotContain "--group-add"
// container root already is the host user — the repair chown must not target the host ids,
// which would push the worktree files into the subuid range
args[args.size - 3] shouldBe "0"
args[args.size - 2] shouldBe "0"
verify {
commandRunner.runOrThrow(
match { it.take(2) == listOf("docker", "run") && it.takeLast(2) == listOf("0", "0") },
repoDir,
)
}
}
test("exposes git metadata read-only with the gittally dir masked for a worktree workspace") {
val gitDir = repoDir.resolve(".git")
val adminDir = gitDir.resolve("worktrees/workspace")
Files.createDirectories(adminDir)
Files.createDirectories(gitDir.resolve("gittally"))
Files.createDirectories(workspace)
Files.writeString(workspace.resolve(".git"), "gitdir: $adminDir\n")
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, dockerBranchConfig())
val args = captured.single()
args shouldContain "$gitDir:$gitDir:ro"
args[args.indexOf("--tmpfs") + 1] shouldBe "${gitDir.resolve("gittally")}"
args shouldContain "$adminDir:$adminDir"
}
test("mounts no git metadata when the workspace is not a worktree") {
runner.start("./gradlew test", workspace, mapOf("branch" to "main"), repoDir, dockerBranchConfig())
val args = captured.single()
args shouldNotContain "--tmpfs"
args.none { it.endsWith(":ro") } shouldBe true
}
test("host network keeps Testcontainers on localhost without add-host") {