Files
hermes-config/skills/gaming/game-streaming/references/headless-server-setup.md
T
2026-07-12 10:17:17 -04:00

9.5 KiB
Raw Blame History

Headless Server Setup for Game Streaming

Full pipeline for running Sunshine + games on a headless Linux server (no monitor attached).

Required components

  1. HDMI dummy plug ($5-10) — plugs into GPU HDMI port, emulates a 4K display. Without it, NVIDIA GPUs run in low-power mode with reduced clocks and may refuse to render at full speed. Essential for headless setups.

    EDID reality check: Many "4K/120" dummy plugs (e.g., FUEARAN) advertise 4K@120Hz on the label but their EDID caps 4K at 60Hz. Actual high-refresh options are 1440p@120Hz and 1080p@120Hz. Check with:

    DISPLAY=:0 xrandr --query | grep -A 20 "^HDMI-0" | grep -E "^[ ]+[0-9]"
    

    If 4K@120Hz is absent despite the label, the plug hardware doesn't support it — plan your game resolutions accordingly.

  2. Xorg — needs to run headless pointing at the dummy display, and persist across reboots.

    Verify the dummy display:

    for conn in /sys/class/drm/card0-*/status; do echo "$conn: $(cat $conn)"; done
    # Look for "connected" — that's your dummy plug
    cat /sys/class/drm/card0-HDMI-A-1/modes | head -1  # should show 3840x2160 or similar
    

    Start Xorg (manual test):

    sudo Xorg :0 -ac -nolisten tcp vt7 &
    sleep 2
    DISPLAY=:0 xdpyinfo | grep dimensions  # verify
    

    Xorg persistence (systemd system service): Xorg needs root for VT access on headless servers. Create a system service so it survives reboots:

    # /etc/systemd/system/xorg-headless.service
    [Unit]
    Description=Xorg display server for headless game streaming
    After=multi-user.target
    
    [Service]
    Type=simple
    User=root
    ExecStart=/usr/bin/Xorg :0 -ac -nolisten tcp vt7
    Restart=on-failure
    RestartSec=3
    
    [Install]
    WantedBy=multi-user.target
    

    Then: sudo systemctl daemon-reload && sudo systemctl enable --now xorg-headless

  3. Sunshine — game streaming server. See main SKILL.md for install.

  4. Game launchers:

    • Steam (+ Proton): Requires 32-bit architecture on Ubuntu 26.04+.

      sudo dpkg --add-architecture i386
      sudo apt update
      sudo apt install -y steam-installer
      

      Headless blocker — zenity license dialog: The steam-installer package installs a /usr/games/steam wrapper script that, on first run, pops up a zenity license dialog (--ok-label=Install --cancel-label=Cancel). On a headless display, nobody can click "Install", so the Steam runtime never downloads. Do not attempt debconf preseeding — the zenity check (lines 113139 of /usr/games/steam) is hardcoded and ignores debconf.

      Workaround — bootstrap Steam manually: Run the scripts/setup-headless-steam.sh script included in this skill. It automatically detects the required version, downloads the bootstrap tarball, unpacks the embedded runtime, and links the directories so the GUI license dialog is bypassed.

      After these steps, DISPLAY=:0 steam launches without any dialog. First run downloads the full Steam client (~200-500 MB) and displays a self-updating progress bar (zenity --progress --auto-close). The client goes through a multi-stage bootstrap: update download → restart → runtime unpack → restart → login screen. This takes 3-5 minutes on a fast connection. Steam must be running and logged in for Sunshine's "Steam Big Picture" app to work.

      Proton is built into Steam — enable in Settings → Steam Play. Handles ~85% of Windows games.

    • Lutris: sudo apt install lutris. For non-Steam games: GOG, Epic Games Store, Battle.net, emulators.

Pipeline

Headless server (no monitor)
  ↳ HDMI dummy plug → GPU sees "4K display" → full clocks enabled
      ↳ Xorg virtual desktop on dummy display
          ↳ Game launches (Steam/Lutris)
              ↳ Sunshine captures X11 display via NVENC
                  ↳ Moonlight on client device streams it

GPU requirements

  • NVIDIA GPU with NVENC (Turing or newer: GTX 1650 Super+, RTX 20-series+, RTX 30-series+, RTX 40-series+)
  • Consumer cards limited to 3 concurrent NVENC sessions
  • NVENC is dedicated silicon — negligible FPS hit during encoding

Linux game compatibility

Type How Compatibility
Native Linux games Steam → Install → Play 100%
Windows games (Proton) Steam → Enable Steam Play → Install ~85% works out of box, ~10% needs tweaks, ~5% broken
Anti-cheat games Valorant, CoD, Fortnite, Destiny 2 0% — kernel-level anti-cheat incompatible with Proton
Non-Steam games Lutris (GOG, Epic, Battle.net) Varies, mostly good

ProtonDB (protondb.com) is the definitive compatibility reference for any specific game.

Audio setup (headless)

Headless servers have no physical speakers and often no audio system at all. Sunshine needs an audio sink to capture game audio and stream it to Moonlight.

Install PipeWire

sudo apt install -y pipewire pipewire-pulse wireplumber pulseaudio-utils

This gives you a PulseAudio-compatible interface over PipeWire 1.6+. The auto_null (Dummy Output) sink is created automatically — it accepts audio from games and makes it available for Sunshine capture without needing physical hardware.

User must be in audio group

sudo usermod -a -G audio $USER
# Requires re-login or systemctl --user daemon-reexec to take effect

Without the audio group, /dev/snd/* devices are permission-denied and PipeWire can't enumerate ALSA hardware. The auto_null sink still works for capture, but NVIDIA HDMI audio (from the dummy plug's EDID) won't appear as a proper hardware sink until the group is active.

Sunshine config for headless dummy plug

Required config — tell Sunshine which display to capture: Without output_name, Sunshine defaults to display index 0 (DP-0), which may be disconnected even with a dummy plug in HDMI-0. The log shows:

Configuring selected display (0) to stream
Couldn't get requested display info, defaulting to recording entire virtual desktop

Fix: add to ~/.config/sunshine/sunshine.conf:

output_name = 2

⚠️ IMPORTANT: output_name takes a numeric display index, NOT the connector name string. Passing HDMI-0 as the value causes Sunshine to fail with Could not stream display number, there are only [8] displays.

Determine the correct index from Sunshine's own log output:

grep "Detected display" ~/.config/sunshine/sunshine.log
# Detected display: DP-0 (id: 0)DP-0 connected: false
# Detected display: DP-1 (id: 1)DP-1 connected: false
# Detected display: HDMI-0 (id: 2)HDMI-0 connected: true  ← use output_name = 2
# Detected display: DP-2 (id: 3)DP-2 connected: false

The (id: N) value is the zero-based display index. Only one display should be connected: true on a headless system with a single dummy plug. After setting, verify Sunshine captures the correct display:

Streaming display: HDMI-0 with res 3840x2160 offset by 0x0

This log line confirms Sunshine is streaming the right display. Required alongside capture = x11 for NVIDIA headless setups.

Do NOT set audio sink config: audio_sink or virtual_sink in sunshine.conf causes an immediate fatal crash (pa_simple_new() failed: Invalid argument) on Sunshine v2026+. Let Sunshine auto-detect PipeWire instead.

CRITICAL FIX FOR CRASHING STREAMS: If Sunshine still crashes with Invalid argument upon stream start, PipeWire is rejecting Sunshine's attempt to dynamically create virtual surround sinks. You must pre-create them so Sunshine finds them instead of crashing:

pactl load-module module-null-sink sink_name=sink-sunshine-stereo
pactl load-module module-null-sink sink_name=sink-sunshine-surround51 channels=6 channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right
pactl load-module module-null-sink sink_name=sink-sunshine-surround71 channels=8 channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right

(Run these commands before Sunshine starts, e.g. in a wrapper script or systemd ExecStartPre).

Verify audio

pactl info | grep "Default Sink"          # should show auto_null
pactl list short sinks                     # should show auto_null, SUSPENDED
journalctl --user -u sunshine | grep -i audio  # should have no warnings/errors

The earlier "Warning: There will be no audio" in sunshine logs should be gone after this config.

Post-reboot: NVIDIA HDMI audio

After a reboot (once the audio group takes effect), WirePlumber discovers the NVIDIA HDMI audio device exposed by the dummy plug. An additional sink like alsa_output.pci-0000_01_00.1.hdmi-stereo appears. Either sink works — games output to the default sink and Sunshine captures whichever one is configured. The real HDMI sink has the advantage of proper EDID-negotiated sample rates (up to 96kHz on the FUERAN dummy plug).

VRAM cohabitation strategy

When the server also runs LLM inference:

Scenario Solution
Model always loaded, game occasionally Socket-activated llama-server (see llama-cpp skill, deployment-patterns.md). Model unloads when idle, 3-4s warm restart.
Gaming always, model occasionally Stop llama-server before gaming. Start manually when needed.
Both simultaneously Only works if VRAM fits both — e.g., 24 GB card with a 7B model (~4.7 GB) leaves ~19 GB for games.

Testing before buying hardware

  • Verify nvidia-smi shows NVENC support (nvidia-smi -q | grep -A5 Encoder)
  • Run Sunshine and confirm web UI at https://localhost:47990
  • Install Moonlight on any device and verify auto-discovery works