Skip to content

Commit 3d96ba4

Browse files
committed
feat(lib): enable graphical GPU access out of the box for gpu=all
Generated dev containers with gpu: "all" now set NVIDIA_DRIVER_CAPABILITIES=all and NVIDIA_VISIBLE_DEVICES=all so the NVIDIA runtime injects the graphics/display libraries (libGLX_nvidia, libEGL_nvidia) at container creation, and the image registers the NVIDIA EGL vendor ICD at /usr/share/glvnd/egl_vendor.d/10_nvidia.json. This removes the manual per-container env edit, recreate, and vendor-JSON copy previously required for graphical GPU/EGL over SSH. Non-GPU projects are unaffected. Edits both source-of-truth template copies (lib + vendored app) and adds tests covering compose env wiring and the Dockerfile EGL ICD registration. Refs #395
1 parent b47351e commit 3d96ba4

8 files changed

Lines changed: 150 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prover-coder-ai/docker-git": minor
3+
---
4+
5+
Make GPU access work out of the box for `gpu: "all"` projects. Generated dev containers now receive `NVIDIA_DRIVER_CAPABILITIES=all` and `NVIDIA_VISIBLE_DEVICES=all` (so the NVIDIA runtime injects the graphics/display libraries — `libGLX_nvidia`, `libEGL_nvidia` — not just compute), and the image registers the NVIDIA EGL vendor ICD at `/usr/share/glvnd/egl_vendor.d/10_nvidia.json`. This removes the manual per-container env edit, recreate, and vendor-JSON copy previously needed to get graphical GPU/EGL working over SSH. Non-GPU projects are unaffected.

.gitkeep

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/app/src/lib/core/templates/docker-compose.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type ComposeFragments = {
2020
readonly maybeGrokAuthLabelEnv: string
2121
readonly maybeAgentModeEnv: string
2222
readonly maybeAgentAutoEnv: string
23+
readonly maybeGpuEnv: string
2324
readonly maybeDependsOn: string
2425
readonly maybeDockerSocketMount: string
2526
readonly maybePlaywrightEnv: string
@@ -108,6 +109,25 @@ const renderGpu = (gpu: TemplateConfig["gpu"]): string =>
108109
? " gpus: all\n"
109110
: ""
110111

112+
// CHANGE: request the full NVIDIA driver capability set for GPU-enabled containers.
113+
// WHY: `gpus: all` alone only exposes compute/utility, so the NVIDIA runtime never injects the
114+
// graphics/display userspace libraries (libGLX_nvidia, libEGL_nvidia). Setting
115+
// NVIDIA_DRIVER_CAPABILITIES=all makes graphical GPU work out of the box, removing the manual
116+
// per-container env edit + recreate that issue-395 documents.
117+
// QUOTE(ТЗ): "В конфиге docker-git ... добавить переменную окружения: NVIDIA_DRIVER_CAPABILITIES=all"
118+
// REF: issue-395
119+
// SOURCE: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html#driver-capabilities
120+
// PURITY: CORE
121+
// INVARIANT: gpu === "all" => both NVIDIA_* vars are emitted; gpu === "none" => empty fragment
122+
// COMPLEXITY: O(1)/O(1)
123+
const renderGpuEnv = (gpu: TemplateConfig["gpu"]): string =>
124+
gpu === "all"
125+
? ` # GPU: request every driver capability (graphics/display, not just compute) so the
126+
# NVIDIA runtime injects EGL/GLX libraries when the container is (re)created.
127+
NVIDIA_VISIBLE_DEVICES: "all"
128+
NVIDIA_DRIVER_CAPABILITIES: "all"\n`
129+
: ""
130+
111131
const renderBootstrapMounts = (): string => ` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`
112132

113133
const renderYamlSingleQuoted = (value: string): string => `'${value.replaceAll("'", "''")}'`
@@ -210,6 +230,7 @@ const buildComposeFragments = (
210230
maybeDockerSocketMount: playwright.maybeDockerSocketMount,
211231
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
212232
maybeBrowserVolume: playwright.maybeBrowserVolume,
233+
maybeGpuEnv: renderGpuEnv(config.gpu),
213234
maybeBootstrapMounts: renderBootstrapMounts(),
214235
forkRepoUrl
215236
}
@@ -231,7 +252,7 @@ ${renderGpu(config.gpu)}${
231252
REPO_URL: "${config.repoUrl}"
232253
REPO_REF: "${config.repoRef}"
233254
FORK_REPO_URL: "${fragments.forkRepoUrl}"
234-
${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
255+
${fragments.maybeGpuEnv}${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
235256
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
236257
${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
237258
${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)

packages/app/src/lib/core/templates/dockerfile.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,32 @@ RUN set -eu; \
8282
rtk --version; \
8383
rtk gain >/dev/null 2>&1 || true`
8484

85+
// CHANGE: register the NVIDIA EGL vendor ICD in GPU-enabled dev images.
86+
// WHY: with NVIDIA_DRIVER_CAPABILITIES=all the runtime injects libEGL_nvidia.so.0 at container
87+
// creation, but it does NOT install the glvnd vendor JSON that points EGL clients at it. Baking
88+
// the tiny static ICD into the image makes graphical EGL resolve without the manual file-copy
89+
// step issue-395 documents. The fragment is empty for non-GPU projects so their images stay lean.
90+
// QUOTE(ТЗ): "ls /usr/share/glvnd/egl_vendor.d/10_nvidia.json"
91+
// REF: issue-395
92+
// SOURCE: https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md
93+
// PURITY: CORE (pure template renderer)
94+
// INVARIANT: gpu === "all" => rendered Dockerfile writes 10_nvidia.json; gpu === "none" => empty fragment
95+
// COMPLEXITY: O(1)/O(1)
96+
const renderDockerfileGpu = (config: TemplateConfig): string =>
97+
config.gpu === "all"
98+
? `# GPU graphics: register the NVIDIA EGL vendor ICD so glvnd resolves libEGL_nvidia at runtime.
99+
# The driver library itself is injected by the NVIDIA runtime (NVIDIA_DRIVER_CAPABILITIES=all).
100+
RUN mkdir -p /usr/share/glvnd/egl_vendor.d \
101+
&& printf '%s\\n' \
102+
'{' \
103+
' "file_format_version" : "1.0.0",' \
104+
' "ICD" : {' \
105+
' "library_path" : "libEGL_nvidia.so.0"' \
106+
' }' \
107+
'}' \
108+
> /usr/share/glvnd/egl_vendor.d/10_nvidia.json`
109+
: ""
110+
85111
const dockerGitSessionSyncPackage = "@prover-coder-ai/docker-git-session-sync@latest"
86112

87113
const renderDockerfilePlaywrightRuntime = (config: TemplateConfig): string =>
@@ -245,6 +271,7 @@ export const renderDockerfile = (config: TemplateConfig): string =>
245271
renderDockerfileBun(config),
246272
renderDockerfilePlaywrightRuntime(config),
247273
renderDockerfileRtk(),
274+
renderDockerfileGpu(config),
248275
renderDockerfileOpenCode(),
249276
renderDockerfileGitleaks(),
250277
renderDockerfileUsers(config),

packages/app/tests/docker-git/core-templates.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,31 @@ describe("app planFiles", () => {
142142
)
143143
})
144144

145+
it("enables graphical GPU access end to end when gpu is set to all", () => {
146+
const files = planFiles(makeTemplateConfig({ gpu: "all" }))
147+
const compose = getGeneratedFile(files, "docker-compose.yml")
148+
const dockerfile = getGeneratedFile(files, "Dockerfile")
149+
150+
expect(compose.contents).toContain(" gpus: all\n")
151+
expect(compose.contents).toContain('NVIDIA_VISIBLE_DEVICES: "all"')
152+
expect(compose.contents).toContain('NVIDIA_DRIVER_CAPABILITIES: "all"')
153+
expect(dockerfile.contents).toContain("mkdir -p /usr/share/glvnd/egl_vendor.d")
154+
expect(dockerfile.contents).toContain('"library_path" : "libEGL_nvidia.so.0"')
155+
expect(dockerfile.contents).toContain("> /usr/share/glvnd/egl_vendor.d/10_nvidia.json")
156+
})
157+
158+
it("omits GPU env and EGL ICD wiring when gpu is none", () => {
159+
const files = planFiles(makeTemplateConfig({ gpu: "none" }))
160+
const compose = getGeneratedFile(files, "docker-compose.yml")
161+
const dockerfile = getGeneratedFile(files, "Dockerfile")
162+
163+
expect(compose.contents).not.toContain(" gpus: all\n")
164+
expect(compose.contents).not.toContain("NVIDIA_DRIVER_CAPABILITIES")
165+
expect(compose.contents).not.toContain("NVIDIA_VISIBLE_DEVICES")
166+
expect(dockerfile.contents).not.toContain("egl_vendor.d/10_nvidia.json")
167+
expect(dockerfile.contents).not.toContain("libEGL_nvidia.so.0")
168+
})
169+
145170
it("keeps plan-to-git state out of generated git and docker contexts", () => {
146171
fc.assert(
147172
fc.property(generatedTemplateConfigArbitrary, (config) => {

packages/lib/src/core/templates/docker-compose.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ComposeFragments = {
1919
readonly maybeGrokAuthLabelEnv: string
2020
readonly maybeAgentModeEnv: string
2121
readonly maybeAgentAutoEnv: string
22+
readonly maybeGpuEnv: string
2223
readonly maybeDependsOn: string
2324
readonly maybeDockerSocketMount: string
2425
readonly maybePlaywrightEnv: string
@@ -107,6 +108,25 @@ const renderGpu = (gpu: TemplateConfig["gpu"]): string =>
107108
? " gpus: all\n"
108109
: ""
109110

111+
// CHANGE: request the full NVIDIA driver capability set for GPU-enabled containers.
112+
// WHY: `gpus: all` alone only exposes compute/utility, so the NVIDIA runtime never injects the
113+
// graphics/display userspace libraries (libGLX_nvidia, libEGL_nvidia). Setting
114+
// NVIDIA_DRIVER_CAPABILITIES=all makes graphical GPU work out of the box, removing the manual
115+
// per-container env edit + recreate that issue-395 documents.
116+
// QUOTE(ТЗ): "В конфиге docker-git ... добавить переменную окружения: NVIDIA_DRIVER_CAPABILITIES=all"
117+
// REF: issue-395
118+
// SOURCE: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html#driver-capabilities
119+
// PURITY: CORE
120+
// INVARIANT: gpu === "all" => both NVIDIA_* vars are emitted; gpu === "none" => empty fragment
121+
// COMPLEXITY: O(1)/O(1)
122+
const renderGpuEnv = (gpu: TemplateConfig["gpu"]): string =>
123+
gpu === "all"
124+
? ` # GPU: request every driver capability (graphics/display, not just compute) so the
125+
# NVIDIA runtime injects EGL/GLX libraries when the container is (re)created.
126+
NVIDIA_VISIBLE_DEVICES: "all"
127+
NVIDIA_DRIVER_CAPABILITIES: "all"\n`
128+
: ""
129+
110130
const renderBootstrapMounts = (): string => ` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`
111131

112132
const renderYamlSingleQuoted = (value: string): string => `'${value.replaceAll("'", "''")}'`
@@ -209,6 +229,7 @@ const buildComposeFragments = (
209229
maybeDockerSocketMount: playwright.maybeDockerSocketMount,
210230
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
211231
maybeBrowserVolume: playwright.maybeBrowserVolume,
232+
maybeGpuEnv: renderGpuEnv(config.gpu),
212233
maybeBootstrapMounts: renderBootstrapMounts(),
213234
forkRepoUrl
214235
}
@@ -230,7 +251,7 @@ ${renderGpu(config.gpu)}${
230251
REPO_URL: "${config.repoUrl}"
231252
REPO_REF: "${config.repoRef}"
232253
FORK_REPO_URL: "${fragments.forkRepoUrl}"
233-
${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
254+
${fragments.maybeGpuEnv}${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
234255
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
235256
${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
236257
${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)

packages/lib/src/core/templates/dockerfile.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,32 @@ RUN set -eu; \
8282
rtk --version; \
8383
rtk gain >/dev/null 2>&1 || true`
8484

85+
// CHANGE: register the NVIDIA EGL vendor ICD in GPU-enabled dev images.
86+
// WHY: with NVIDIA_DRIVER_CAPABILITIES=all the runtime injects libEGL_nvidia.so.0 at container
87+
// creation, but it does NOT install the glvnd vendor JSON that points EGL clients at it. Baking
88+
// the tiny static ICD into the image makes graphical EGL resolve without the manual file-copy
89+
// step issue-395 documents. The fragment is empty for non-GPU projects so their images stay lean.
90+
// QUOTE(ТЗ): "ls /usr/share/glvnd/egl_vendor.d/10_nvidia.json"
91+
// REF: issue-395
92+
// SOURCE: https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md
93+
// PURITY: CORE (pure template renderer)
94+
// INVARIANT: gpu === "all" => rendered Dockerfile writes 10_nvidia.json; gpu === "none" => empty fragment
95+
// COMPLEXITY: O(1)/O(1)
96+
const renderDockerfileGpu = (config: TemplateConfig): string =>
97+
config.gpu === "all"
98+
? `# GPU graphics: register the NVIDIA EGL vendor ICD so glvnd resolves libEGL_nvidia at runtime.
99+
# The driver library itself is injected by the NVIDIA runtime (NVIDIA_DRIVER_CAPABILITIES=all).
100+
RUN mkdir -p /usr/share/glvnd/egl_vendor.d \
101+
&& printf '%s\\n' \
102+
'{' \
103+
' "file_format_version" : "1.0.0",' \
104+
' "ICD" : {' \
105+
' "library_path" : "libEGL_nvidia.so.0"' \
106+
' }' \
107+
'}' \
108+
> /usr/share/glvnd/egl_vendor.d/10_nvidia.json`
109+
: ""
110+
85111
const dockerGitSessionSyncPackage = "@prover-coder-ai/docker-git-session-sync@latest"
86112

87113
const renderDockerfilePlaywrightRuntime = (config: TemplateConfig): string =>
@@ -245,6 +271,7 @@ export const renderDockerfile = (config: TemplateConfig): string =>
245271
renderDockerfileBun(config),
246272
renderDockerfilePlaywrightRuntime(config),
247273
renderDockerfileRtk(),
274+
renderDockerfileGpu(config),
248275
renderDockerfileOpenCode(),
249276
renderDockerfileGitleaks(),
250277
renderDockerfileUsers(config),

packages/lib/tests/core/templates.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ describe("renderDockerfile", () => {
249249
expect(dockerfile).not.toContain("playwright-mcp --cdp-endpoint")
250250
expect(dockerfile).not.toContain("MCP_PLAYWRIGHT_CDP_TIMEOUT")
251251
})
252+
253+
it("omits the NVIDIA EGL vendor ICD for non-GPU projects", () => {
254+
const dockerfile = renderDockerfile(makeTemplateConfig({ gpu: "none" }))
255+
256+
expect(dockerfile).not.toContain("egl_vendor.d/10_nvidia.json")
257+
expect(dockerfile).not.toContain("libEGL_nvidia.so.0")
258+
})
259+
260+
it("registers the NVIDIA EGL vendor ICD when GPU access is enabled", () => {
261+
const dockerfile = renderDockerfile(makeTemplateConfig({ gpu: "all" }))
262+
263+
expectContainsAll(dockerfile, [
264+
"mkdir -p /usr/share/glvnd/egl_vendor.d",
265+
'"library_path" : "libEGL_nvidia.so.0"',
266+
"> /usr/share/glvnd/egl_vendor.d/10_nvidia.json"
267+
])
268+
})
252269
})
253270

254271
describe("renderPromptScript", () => {
@@ -874,6 +891,8 @@ describe("renderDockerCompose", () => {
874891
expect(compose).toContain(' extra_hosts:\n - "host.docker.internal:host-gateway"')
875892
expect(compose).toContain(" dns:\n - 8.8.8.8\n - 8.8.4.4\n - 1.1.1.1\n networks:")
876893
expect(compose).not.toContain(" gpus: all\n")
894+
expect(compose).not.toContain("NVIDIA_DRIVER_CAPABILITIES")
895+
expect(compose).not.toContain("NVIDIA_VISIBLE_DEVICES")
877896
expect(compose).not.toContain("dg-test-browser")
878897
expect(compose).not.toContain("/var/run/docker.sock:/var/run/docker.sock")
879898
expect((compose.match(/\n dns:\n/g) ?? []).length).toBe(1)
@@ -902,6 +921,9 @@ describe("renderDockerCompose", () => {
902921

903922
expect(compose).toContain(" gpus: all\n")
904923
expect((compose.match(/\n gpus: all\n/g) ?? []).length).toBe(1)
924+
expect(compose).toContain('NVIDIA_VISIBLE_DEVICES: "all"')
925+
expect(compose).toContain('NVIDIA_DRIVER_CAPABILITIES: "all"')
926+
expect((compose.match(/NVIDIA_DRIVER_CAPABILITIES: "all"/g) ?? []).length).toBe(1)
905927
expect(compose).toContain('DOCKER_GIT_BROWSER_CONTAINER_NAME: "dg-test-browser"')
906928
expect(compose).not.toContain("\n dg-test-browser:\n")
907929
})

0 commit comments

Comments
 (0)