190 lines
9.5 KiB
Markdown
190 lines
9.5 KiB
Markdown
# 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
|