steam-headless Not Rendering On NVIDIA¶
Runbook for the Grafana alerts Steam Headless Not Rendering On NVIDIA
(steam-headless-glx-not-nvidia), Steam Headless NVIDIA Driver Pin Stale
(steam-headless-driver-pin-stale) and Steam Headless GLX Probe Stale
(steam-headless-glx-probe-stale), group steam-headless-alerts.
What the alert means¶
steam-headless on big-boi runs a full Xorg desktop against the RTX 3070. When OpenGL inside that
container is not served by the NVIDIA driver, Steam loses CUDA/GL interop, reports
NVENC - No CUDA support, falls back to a VAAPI encoder that has no driver behind it, and segfaults
on every Steam Link connection.
Nothing else goes red when this happens. The container stays up, the desktop comes up, nvidia-smi
still lists the GPU, and the host's nvidia-gpu-exporter metrics stay healthy. The only signal is
this alert.
How it is measured¶
/opt/big-boi/steam/glx-probe.sh runs every 5 minutes from /etc/cron.d/big-boi-steam-glx-probe.
It runs docker exec steam-headless glxinfo -B and writes three gauges to node_exporter's textfile
collector at /var/lib/node_exporter/textfile_collectors/steam_headless_glx.prom:
| Metric | Meaning |
|---|---|
steam_headless_running |
The container is running. |
steam_headless_glx_nvidia |
glxinfo reported OpenGL vendor string: NVIDIA Corporation. |
steam_headless_driver_mismatch |
The container's NVIDIA_DRIVER_VERSION differs from the host's loaded kernel module (/proc/driver/nvidia/version). |
The alert condition is steam_headless_running - steam_headless_glx_nvidia > 0.5, which is 1 only
when the container is up without NVIDIA GL, and 0 when it is healthy or deliberately stopped. It
has a for: 15m, the group evaluates every 5 minutes, and the notification policy repeats every 4
hours, so expect roughly 20 minutes from cause to Discord and a repeat every 4 hours after that.
steam-headless-glx-probe-stale is the companion: it fires when node_textfile_mtime_seconds for
that file is more than 30 minutes behind, which means the probe itself is broken rather than the GPU.
Its noDataState is Alerting, so it also fires when the series is absent entirely — see
Triage: probe stale, because a current .prom file on disk is not enough.
The resolve notification does not mean anything was fixed
/opt/big-boi/steam/daily-restart.sh restarts the container at 05:00 America/New_York every
day. That restart clears causes 1 and 2, so an alert that fires overnight resolves
on its own in the morning whether or not anyone looked at it. If you got a resolve you did not
earn, the cause is still there and it will happen again.
Triage¶
All three causes look identical from the metric. Xorg's own log is what separates them. It is on a bind mount, so it survives container restarts:
ssh mcp-admin@bigboi.heezy.local -t "grep -nE 'glxserver|Failed to load' /opt/big-boi/steam-headless/.cache/log/xorg.err.log | tail -20"
The file is appended across restarts, so read the last block. A healthy Xorg start looks like this:
(II) Loading /usr/lib/x86_64-linux-gnu/nvidia/xorg/libglxserver_nvidia.so
(II) Module glxserver_nvidia: vendor="NVIDIA Corporation"
(II) NVIDIA GLX Module 595.84
(II) GLX: Another vendor is already registered for screen 0 appears on every healthy start. Ignore
it.
1. Device access stripped by a systemd reload¶
Most common. On cgroup v2 with the systemd cgroup driver, systemctl daemon-reload resets the
eBPF device filter for containers Docker started, and the container silently loses /dev/nvidia*.
Xorg already holds its file descriptors, so it keeps running and logs nothing. Every process started
afterwards, glxinfo and Steam's renderer included, cannot open the device and falls back to Mesa.
Signature:
- The last Xorg block in
xorg.err.logloaded NVIDIA GLX cleanly. - No Xorg restart in the container log across the whole alert window.
systemd[1]: Reloading.in the host journal shortly before the first bad probe.
glxinfo does not always name Mesa when this happens. It can fail outright instead, which counts
the same as far as the probe is concerned:
X Error of failed request: BadValue (integer parameter out of range for operation)
Major opcode of failed request: 151 (GLX)
Minor opcode of failed request: 24 (X_GLXCreateNewContext)
The server side is fine in that state — glxserver_nvidia is loaded and the versions match — so
this is still cause 1, not cause 3. Check xorg.err.log before assuming a driver problem.
ssh mcp-admin@bigboi.heezy.local -t "sudo journalctl --since '-12h' | grep -E 'Reloading\.|ansible-systemd'"
Any Ansible run that touches a systemd unit on big-boi does this, including playbooks/baseline.yml,
which installs node_exporter with daemon_reload: yes on every run.
2. Xorg gave up and supervisord stopped retrying¶
If device access is lost while Xorg is starting rather than while it is running, Xorg cannot initialise the kernel module, supervisord retries four times in six seconds and gives up. The container then sits there healthy by every other measure with no X server in it.
Signature, in docker logs steam-headless:
3. Driver mismatch after a version bump¶
steam-headless-driver-pin-stale fires alongside the Mesa alert. gpu.yml reads the host driver
version when the big-boi playbook runs and templates it into /opt/big-boi/steam/docker-compose.yml
as NVIDIA_DRIVER_VERSION. If the host driver moves without a playbook run, the container keeps
installing the old userspace and Xorg segfaults loading its GLX module against the host's newer
nvidia_drv.so:
(II) NVIDIA dlloader X Driver <new version>
(II) NVIDIA GLX Module <old version>
(EE) Segmentation fault at address 0x0
supervisord then gives up on xorg (FATAL), so there is no X server at all. A restart reuses the
same pin and changes nothing.
roles/big-boi/tasks/nvidia.yml blacklists the NVIDIA packages from unattended-upgrades, so the
driver should only move through that role. If this fires anyway, dispatch the big-boi playbook, then
find what changed the driver.
A second, older form of this cause is the image's stale Debian GLX symlink. fix-nvidia-glxserver.sh
repoints it on every container start. Its signature is a Failed to load module "glxserver_nvidia"
line rather than a segfault.
Fix¶
Causes 1 and 2 are both fixed by a restart, which re-creates the device allowlist and starts a fresh Xorg:
A container whose GL is broken cannot be running a game, so this is not disruptive. Give it about a minute, then confirm:
ssh mcp-admin@bigboi.heezy.local -t "sudo docker exec steam-headless glxinfo -B | grep -E 'vendor|renderer'"
Healthy output names the GPU, not Mesa or llvmpipe:
The probe picks this up within 5 minutes and the alert clears on the next evaluation. To confirm without waiting for Grafana:
ssh mcp-admin@bigboi.heezy.local -t "sudo /opt/big-boi/steam/glx-probe.sh; cat /var/lib/node_exporter/textfile_collectors/steam_headless_glx.prom"
Cause 3 is a playbook run, not a restart. Dispatch the big-boi playbook through Gitea Actions.
Reattaching from Ansible¶
playbooks/big-boi.yml already handles this. Its post_tasks import
roles/big-boi/tasks/gpu-verify.yml, which runs after handlers flush, checks NVML and GLX in every
GPU container, and restarts only the ones that are actually broken. A healthy run restarts nothing.
Running that playbook is a valid fix for causes 1 and 2 if you would rather not touch the host by
hand.
playbooks/baseline.yml and playbooks/lgtm.yml also reach big-boi and reload systemd, so both
run the same gpu-verify post_tasks, limited to big-boi.
Triage: probe stale¶
Check the metric before the file. The two can disagree, and when they do the file is the misleading one.
ssh mcp-admin@bigboi.heezy.local -t "curl -s localhost:9100/metrics | grep -e node_textfile -e steam_headless"
The series is missing entirely¶
If node_textfile_mtime_seconds is absent while
/var/lib/node_exporter/textfile_collectors/steam_headless_glx.prom is current on disk, node_exporter
is not reading the directory. Check the unit:
It must carry --collector.textfile.directory. Without the flag the collector is off, every textfile
metric vanishes, and with noDataState: Alerting the rule fires. The unit belongs to the
prometheus-node-exporter role and nothing else may write it; a second writer that omits the flag
is how this happened on 2026-08-25. Re-run playbooks/baseline.yml, which runs that role for every
Linux host.
The series is present but stale¶
Then the probe really is not running. Check the cron and the script:
ssh mcp-admin@bigboi.heezy.local -t "cat /etc/cron.d/big-boi-steam-glx-probe; sudo /opt/big-boi/steam/glx-probe.sh; echo rc=$?"
The script exits non-zero without writing if the textfile directory does not exist, which is deliberate: publishing into a void would leave the alert quiet for the wrong reason.
The probe cannot tell you why¶
glx-probe.sh sets steam_headless_glx_nvidia=0 on any failure of
timeout 30 docker exec steam-headless glxinfo -B, including an exec that fails or times out for
reasons unrelated to the GPU. A broken probe and a real Mesa fallback publish the same value, which
is why the triage above starts at xorg.err.log rather than at the metric.