# 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:
   ```bash
   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:**
   ```bash
   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):**
   ```bash
   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:
   ```ini
   # /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+.
     ```bash
     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 113–139 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

```bash
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

```bash
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:
```bash
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:

```bash
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

```bash
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
