Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gpu-graphics-out-of-the-box.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@prover-coder-ai/docker-git": minor
---

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.
23 changes: 22 additions & 1 deletion packages/app/src/lib/core/templates/docker-compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ComposeFragments = {
readonly maybeGrokAuthLabelEnv: string
readonly maybeAgentModeEnv: string
readonly maybeAgentAutoEnv: string
readonly maybeGpuEnv: string
readonly maybeDependsOn: string
readonly maybeDockerSocketMount: string
readonly maybePlaywrightEnv: string
Expand Down Expand Up @@ -108,6 +109,25 @@ const renderGpu = (gpu: TemplateConfig["gpu"]): string =>
? " gpus: all\n"
: ""

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

const renderBootstrapMounts = (): string => ` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`

const renderYamlSingleQuoted = (value: string): string => `'${value.replaceAll("'", "''")}'`
Expand Down Expand Up @@ -210,6 +230,7 @@ const buildComposeFragments = (
maybeDockerSocketMount: playwright.maybeDockerSocketMount,
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
maybeBrowserVolume: playwright.maybeBrowserVolume,
maybeGpuEnv: renderGpuEnv(config.gpu),
maybeBootstrapMounts: renderBootstrapMounts(),
forkRepoUrl
}
Expand All @@ -231,7 +252,7 @@ ${renderGpu(config.gpu)}${
REPO_URL: "${config.repoUrl}"
REPO_REF: "${config.repoRef}"
FORK_REPO_URL: "${fragments.forkRepoUrl}"
${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
${fragments.maybeGpuEnv}${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
Expand Down
27 changes: 27 additions & 0 deletions packages/app/src/lib/core/templates/dockerfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ RUN set -eu; \
rtk --version; \
rtk gain >/dev/null 2>&1 || true`

// CHANGE: register the NVIDIA EGL vendor ICD in GPU-enabled dev images.
// WHY: with NVIDIA_DRIVER_CAPABILITIES=all the runtime injects libEGL_nvidia.so.0 at container
// creation, but it does NOT install the glvnd vendor JSON that points EGL clients at it. Baking
// the tiny static ICD into the image makes graphical EGL resolve without the manual file-copy
// step issue-395 documents. The fragment is empty for non-GPU projects so their images stay lean.
// QUOTE(ТЗ): "ls /usr/share/glvnd/egl_vendor.d/10_nvidia.json"
// REF: issue-395
// SOURCE: https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md
// PURITY: CORE (pure template renderer)
// INVARIANT: gpu === "all" => rendered Dockerfile writes 10_nvidia.json; gpu === "none" => empty fragment
// COMPLEXITY: O(1)/O(1)
const renderDockerfileGpu = (config: TemplateConfig): string =>
config.gpu === "all"
? `# GPU graphics: register the NVIDIA EGL vendor ICD so glvnd resolves libEGL_nvidia at runtime.
# The driver library itself is injected by the NVIDIA runtime (NVIDIA_DRIVER_CAPABILITIES=all).
RUN mkdir -p /usr/share/glvnd/egl_vendor.d \
&& printf '%s\\n' \
'{' \
' "file_format_version" : "1.0.0",' \
' "ICD" : {' \
' "library_path" : "libEGL_nvidia.so.0"' \
' }' \
'}' \
> /usr/share/glvnd/egl_vendor.d/10_nvidia.json`
: ""

const dockerGitSessionSyncPackage = "@prover-coder-ai/docker-git-session-sync@latest"

const renderDockerfilePlaywrightRuntime = (config: TemplateConfig): string =>
Expand Down Expand Up @@ -245,6 +271,7 @@ export const renderDockerfile = (config: TemplateConfig): string =>
renderDockerfileBun(config),
renderDockerfilePlaywrightRuntime(config),
renderDockerfileRtk(),
renderDockerfileGpu(config),
renderDockerfileOpenCode(),
renderDockerfileGitleaks(),
renderDockerfileUsers(config),
Expand Down
25 changes: 25 additions & 0 deletions packages/app/tests/docker-git/core-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,31 @@ describe("app planFiles", () => {
)
})

it("enables graphical GPU access end to end when gpu is set to all", () => {
const files = planFiles(makeTemplateConfig({ gpu: "all" }))
const compose = getGeneratedFile(files, "docker-compose.yml")
const dockerfile = getGeneratedFile(files, "Dockerfile")

expect(compose.contents).toContain(" gpus: all\n")
expect(compose.contents).toContain('NVIDIA_VISIBLE_DEVICES: "all"')
expect(compose.contents).toContain('NVIDIA_DRIVER_CAPABILITIES: "all"')
expect(dockerfile.contents).toContain("mkdir -p /usr/share/glvnd/egl_vendor.d")
expect(dockerfile.contents).toContain('"library_path" : "libEGL_nvidia.so.0"')
expect(dockerfile.contents).toContain("> /usr/share/glvnd/egl_vendor.d/10_nvidia.json")
})

it("omits GPU env and EGL ICD wiring when gpu is none", () => {
const files = planFiles(makeTemplateConfig({ gpu: "none" }))
const compose = getGeneratedFile(files, "docker-compose.yml")
const dockerfile = getGeneratedFile(files, "Dockerfile")

expect(compose.contents).not.toContain(" gpus: all\n")
expect(compose.contents).not.toContain("NVIDIA_DRIVER_CAPABILITIES")
expect(compose.contents).not.toContain("NVIDIA_VISIBLE_DEVICES")
expect(dockerfile.contents).not.toContain("egl_vendor.d/10_nvidia.json")
expect(dockerfile.contents).not.toContain("libEGL_nvidia.so.0")
})

it("keeps plan-to-git state out of generated git and docker contexts", () => {
fc.assert(
fc.property(generatedTemplateConfigArbitrary, (config) => {
Expand Down
23 changes: 22 additions & 1 deletion packages/lib/src/core/templates/docker-compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ComposeFragments = {
readonly maybeGrokAuthLabelEnv: string
readonly maybeAgentModeEnv: string
readonly maybeAgentAutoEnv: string
readonly maybeGpuEnv: string
readonly maybeDependsOn: string
readonly maybeDockerSocketMount: string
readonly maybePlaywrightEnv: string
Expand Down Expand Up @@ -107,6 +108,25 @@ const renderGpu = (gpu: TemplateConfig["gpu"]): string =>
? " gpus: all\n"
: ""

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

const renderBootstrapMounts = (): string => ` - ${bootstrapVolumeKey}:/opt/docker-git/bootstrap/source:ro`

const renderYamlSingleQuoted = (value: string): string => `'${value.replaceAll("'", "''")}'`
Expand Down Expand Up @@ -209,6 +229,7 @@ const buildComposeFragments = (
maybeDockerSocketMount: playwright.maybeDockerSocketMount,
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
maybeBrowserVolume: playwright.maybeBrowserVolume,
maybeGpuEnv: renderGpuEnv(config.gpu),
maybeBootstrapMounts: renderBootstrapMounts(),
forkRepoUrl
}
Expand All @@ -230,7 +251,7 @@ ${renderGpu(config.gpu)}${
REPO_URL: "${config.repoUrl}"
REPO_REF: "${config.repoRef}"
FORK_REPO_URL: "${fragments.forkRepoUrl}"
${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
${fragments.maybeGpuEnv}${fragments.maybeGithubAuthSkipEnv} # Optional anonymous public GitHub clone override
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
Expand Down
27 changes: 27 additions & 0 deletions packages/lib/src/core/templates/dockerfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ RUN set -eu; \
rtk --version; \
rtk gain >/dev/null 2>&1 || true`

// CHANGE: register the NVIDIA EGL vendor ICD in GPU-enabled dev images.
// WHY: with NVIDIA_DRIVER_CAPABILITIES=all the runtime injects libEGL_nvidia.so.0 at container
// creation, but it does NOT install the glvnd vendor JSON that points EGL clients at it. Baking
// the tiny static ICD into the image makes graphical EGL resolve without the manual file-copy
// step issue-395 documents. The fragment is empty for non-GPU projects so their images stay lean.
// QUOTE(ТЗ): "ls /usr/share/glvnd/egl_vendor.d/10_nvidia.json"
// REF: issue-395
// SOURCE: https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md
// PURITY: CORE (pure template renderer)
// INVARIANT: gpu === "all" => rendered Dockerfile writes 10_nvidia.json; gpu === "none" => empty fragment
// COMPLEXITY: O(1)/O(1)
const renderDockerfileGpu = (config: TemplateConfig): string =>
config.gpu === "all"
? `# GPU graphics: register the NVIDIA EGL vendor ICD so glvnd resolves libEGL_nvidia at runtime.
# The driver library itself is injected by the NVIDIA runtime (NVIDIA_DRIVER_CAPABILITIES=all).
RUN mkdir -p /usr/share/glvnd/egl_vendor.d \
&& printf '%s\\n' \
'{' \
' "file_format_version" : "1.0.0",' \
' "ICD" : {' \
' "library_path" : "libEGL_nvidia.so.0"' \
' }' \
'}' \
> /usr/share/glvnd/egl_vendor.d/10_nvidia.json`
: ""

const dockerGitSessionSyncPackage = "@prover-coder-ai/docker-git-session-sync@latest"

const renderDockerfilePlaywrightRuntime = (config: TemplateConfig): string =>
Expand Down Expand Up @@ -245,6 +271,7 @@ export const renderDockerfile = (config: TemplateConfig): string =>
renderDockerfileBun(config),
renderDockerfilePlaywrightRuntime(config),
renderDockerfileRtk(),
renderDockerfileGpu(config),
renderDockerfileOpenCode(),
renderDockerfileGitleaks(),
renderDockerfileUsers(config),
Expand Down
22 changes: 22 additions & 0 deletions packages/lib/tests/core/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,23 @@ describe("renderDockerfile", () => {
expect(dockerfile).not.toContain("playwright-mcp --cdp-endpoint")
expect(dockerfile).not.toContain("MCP_PLAYWRIGHT_CDP_TIMEOUT")
})

it("omits the NVIDIA EGL vendor ICD for non-GPU projects", () => {
const dockerfile = renderDockerfile(makeTemplateConfig({ gpu: "none" }))

expect(dockerfile).not.toContain("egl_vendor.d/10_nvidia.json")
expect(dockerfile).not.toContain("libEGL_nvidia.so.0")
})

it("registers the NVIDIA EGL vendor ICD when GPU access is enabled", () => {
const dockerfile = renderDockerfile(makeTemplateConfig({ gpu: "all" }))

expectContainsAll(dockerfile, [
"mkdir -p /usr/share/glvnd/egl_vendor.d",
'"library_path" : "libEGL_nvidia.so.0"',
"> /usr/share/glvnd/egl_vendor.d/10_nvidia.json"
])
})
})

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

expect(compose).toContain(" gpus: all\n")
expect((compose.match(/\n gpus: all\n/g) ?? []).length).toBe(1)
expect(compose).toContain('NVIDIA_VISIBLE_DEVICES: "all"')
expect(compose).toContain('NVIDIA_DRIVER_CAPABILITIES: "all"')
expect((compose.match(/NVIDIA_DRIVER_CAPABILITIES: "all"/g) ?? []).length).toBe(1)
expect(compose).toContain('DOCKER_GIT_BROWSER_CONTAINER_NAME: "dg-test-browser"')
expect(compose).not.toContain("\n dg-test-browser:\n")
})
Expand Down
Loading