在 LXC 中运行 Headless Steam 实现在家里随便一台电脑上,用服务器的显卡玩游戏。最终的效果就是,在 Steam 上面,从家里的服务器 stream 游戏到另一台电脑,手机,或者 Shield 上面。同时,这个服务器上面的显卡还要保留跑 AI 的能力。

Context

之前的文章已经提到了如何在 LXC 中使用 Nvidia GPU device。既然手上有一张 3090,那必然是既要用它玩游戏,也要用它跑跑 AI 的。

相关文章:

虽然在在之前的文章中提到过不建议在 LXC 里面跑 Docker,但是为了能够复用 消费级的 GPU,在LXC 上面跑 Docker 基本上是跑不掉了。所以这里就建议 Proxmox 的 LXC 跑在 EXT4 这种简单的 FS 上面,如果是 ZFS,那大概需要 NVME 才能运行良好了。

这种折腾从某个角度来看是完全没有必要的。如果就是在一个 Ubuntu box 上面跑多个 Container,既有 Ollama,也有 Steamheadless docker,这完全不足为奇。这么折腾的原因仅仅是因为想要保持不同 Application 之间的“独立性”,保持 Proxmox 上面针对每个 GPU Application 的 Snapshot 和 Backup/Recovery 的能力。如果我们只是进行 GPU Passthrough (这篇文章),那么自然可以使用单独一个 VM 跑多个 Docker。但是这样就限制了使用 Proxmox 来单独对于每个 GPU Application 管理的的能力,而且也导致未来所有的 GPU 使用都要在这台 VM 上面进行,这种限制有时候还是挺麻烦的。

另外,如果想真的玩好,还是强烈建议给这个 GPU 接上显示器,或者接上一个 Dummy Display Plug。否则的话,Steam-headless 里面需要模拟一个 Display 设备,这样会导致一些设置不可用,而且极大(非常大)限制 GPU 性能的发挥。Amazon 上面搜 “display port dummy plug” 就可以了。

Set up Steam-Headless

这个实际上已经有现成的了。这次的远程玩游戏的功能就是由这个 project 提供的。

然后这篇.md文档还是要好好看看的,这里面的各种 Directory 呀,还是建议老老实实的按照文档都创建好,毕竟这个 Docker Container 的复杂度确实比较高。另外,很多相关的设置都在这个.env文件里面,需要仔细阅读。

最终跑通的版本是这样的:

  • Docker compose 选择的是 non-privileged 的版本。
  • 选择了逐一map每个GPU 用到的 devices: /dev/nvidia*。按理说安装了 container toolkit 应该可以省略这一步,但是实践发现还是逐个 map 能跑通。
  • 特别注意这里的所有 devices,这些都是在 LXC config 里面需要特殊照顾的。
version: "3.8"
 
services:
  steam-headless:
    image: josh5/steam-headless:latest
    restart: unless-stopped
    shm_size: ${SHM_SIZE}
    ipc: host # Could also be set to 'shareable'
    ulimits:
      nofile:
        soft: 1024
        hard: 524288
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
      - SYS_NICE
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
 
    # GPU PASSTHROUGH
    deploy:
      resources:
        reservations:
          # Enable support for NVIDIA GPUs.
          # 
          # Ref: https://docs.docker.com/compose/gpu-support/#enabling-gpu-access-to-service-containers
          devices:
            - capabilities: [gpu]
              device_ids: ["${NVIDIA_VISIBLE_DEVICES}"]
 
    # NETWORK:
    ## NOTE:  With this configuration, if we do not use the host network, then physical device input
    ##        is not possible and your USB connected controllers will not work in steam games.
    network_mode: host
    hostname: ${NAME}
    extra_hosts:
      - "${NAME}:127.0.0.1"
    
    # ENVIRONMENT:
    ## Read all config variables from the .env file
    environment:
      # System
      - TZ=${TZ}
      - USER_LOCALES=${USER_LOCALES}
      - DISPLAY=${DISPLAY}
      # User
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=${UMASK}
      - USER_PASSWORD=${USER_PASSWORD}
      # Mode
      - MODE=${MODE}
      # Web UI
      - WEB_UI_MODE=${WEB_UI_MODE}
      - ENABLE_VNC_AUDIO=${ENABLE_VNC_AUDIO}
      - PORT_NOVNC_WEB=${PORT_NOVNC_WEB}
      - NEKO_NAT1TO1=${NEKO_NAT1TO1}
      # Steam
      - ENABLE_STEAM=${ENABLE_STEAM}
      - STEAM_ARGS=${STEAM_ARGS}
      # Sunshine
      - ENABLE_SUNSHINE=${ENABLE_SUNSHINE}
      - SUNSHINE_USER=${SUNSHINE_USER}
      - SUNSHINE_PASS=${SUNSHINE_PASS}
      # Xorg
      - ENABLE_EVDEV_INPUTS=${ENABLE_EVDEV_INPUTS}
      - FORCE_X11_DUMMY_CONFIG=${FORCE_X11_DUMMY_CONFIG}
      # Nvidia specific config
      - NVIDIA_DRIVER_CAPABILITIES=${NVIDIA_DRIVER_CAPABILITIES}
      - NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES}
      - NVIDIA_DRIVER_VERSION=${NVIDIA_DRIVER_VERSION}
 
    # DEVICES:
    devices:
      # Use the host fuse device [REQUIRED].
      - /dev/fuse
      # Add the host uinput device [REQUIRED].
      - /dev/uinput
      # Add NVIDIA HW accelerated devices [OPTIONAL].
      # NOTE: If you use the nvidia container toolkit, this is not needed.
      #       Installing the nvidia container toolkit is the recommended method for running this container
      - /dev/nvidia0
      - /dev/nvidiactl
      - /dev/nvidia-modeset
      - /dev/nvidia-uvm
      - /dev/nvidia-uvm-tools
      - /dev/nvidia-caps/nvidia-cap1
      - /dev/nvidia-caps/nvidia-cap2
    # Ensure container access to devices 13:*
    device_cgroup_rules:
      - 'c 13:* rmw'
 
    # VOLUMES:
    volumes:
      # The location of your home directory.
      - ${HOME_DIR}/:/home/default/:rw
 
      # The location where all games should be installed.
      # This path needs to be set as a library path in Steam after logging in.
      # Otherwise, Steam will store games in the home directory above.
      - ${GAMES_DIR}/:/mnt/games/:rw
 
      # The Xorg socket.
      - ${SHARED_SOCKETS_DIR}/.X11-unix/:/tmp/.X11-unix/:rw
 
      # Pulse audio socket.
      - ${SHARED_SOCKETS_DIR}/pulse/:/tmp/pulse/:rw
  • .env 文件, web UI 选择 VNC (Neko 跑不通)
  • FORCE_X11_DUMMY_CONFIG set to false,因为已经用上了 dummy plug,否则如果没有显示器就需要选择 true;
  • NVIDIA_DRIVER_CAPABILITIES 选择的是 all,试验过 graphics,似乎性能损失很大。有 dummy plug 或者显示器的情况下,而且 LXC 的 device mapping 也干脆都拿过来的情况下,还是直接用 all 比较靠谱。
  • PUID 和 PGID 都是1000
  • 按照说明,在 LXC 下面把 /opt/container-services/steam-headless 和 /opt/container-data/steam-headless/ 都创建好。并且 chown -R 1000:1000.
  • 把 docker-compose.yaml 和 .env 文件都放在 /opt/container-services/steam-headless/
  • docker compose up check log,看看是不是有 error,是不是缺少了什么 device,或者 fuse 有问题,或者 AppArmor 有问题。一切跑通之后就用 docker compose up -d 即可。

Set up LXC

最精髓的地方实际上在这里:

  • LXC 必须是 privileged。否则没办法跑 Docker。
  • LXC Options 里面必须要把 Features 里面的 Fuse, mknode, nesting 都打开。

lxc-features

  • /etc/pve/lxc/###.conf 的内容如下:
arch: amd64
cores: 12
features: fuse=1,mknod=1,nesting=1
hostname: testbox
memory: 16384
net0: name=eth0,bridge=vmbr2,firewall=1,hwaddr=XX:XX:XX:XX:XX:XX,ip=dhcp,type=veth
ostype: ubuntu
rootfs: vmpool:subvol-201-disk-0,mountoptions=noatime,size=96G
swap: 0
lxc.cgroup2.devices.allow: c 195:* rwm
lxc.cgroup2.devices.allow: c 507:* rwm
lxc.cgroup2.devices.allow: c 239:* rwm
lxc.mount.entry: /dev/nvidia-caps/nvidia-cap1 dev/nvidia-caps/nvidia-cap1 none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-caps/nvidia-cap2 dev/nvidia-caps/nvidia-cap2 none bind,optional,create=file
lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-modeset dev/nvidia-modeset none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
lxc.cgroup2.devices.allow: c 10:223 rwm
lxc.mount.entry: /dev/uinput dev/uinput none bind,optional,create=file
  • 其中 Nvidia 相关的东西很多,为了方便,通通 map 到 LXC 里面。
  • /dev/uinput 也是要映射进去的。
  • lxc.cgroup2.devices.allow 后面的数字都是 device identifier number (major number),跟 ls -al /dev/nvidia* 里面给出来的保持一致。这些设置基本上在之前的文章中已经提到过了。239 是 /dev/nvidia-caps/nvidia-cap1|2 的 major number,因为 cap1 和 cap2 也 map 进去了,所以干脆一并把 permission 加上。另外 /dev/uinput 的 major number 是 10 (ls -al /dev/uinput ),所以也在后面加上了。
  • (其实这里并不是 很确定是不是需要这么多 permission,尤其是 caps 上面,也许并不需要给 permission。)

其他的关于 Nvidia Driver 方面,就跟之前的文章一样,需要保证 Proxmox 主机上面有 GPU Driver,同时 LXC 里面有一个 kernel-less driver,并且是同版本的。判定方法还是 nvidia-smi: [set-up-a-new-proxmox-server]

如此一来,在 Docker compose 文件中提到的那些 Nvidia 的设备,就都在 LXC 里面映射完毕了。理论上Steam headless 就应该可以运行了。

Results

访问 http://<your_lxc_ip>:8083/web/ 应当就可以进入 steam-headless 的 web UI 了。这里面实际上运行了一个 Linux,需要我们打开已经预装的 Steam,下载游戏,设置好 Proton 最为兼容层,就可以了。

![start-page]](../../assets/posts/2024/headless-steam-in-lxc-with-gpu-image-1.png)

找到 Steam,然后登陆,下载,搞定兼容层设置,开启 stream,设置配对等等。

menu-steam

分辨率大概率还是要调一下的。这里为了Web UI 访问方便就只用了1080p。要求不高的话,玩游戏其实也OK了。

display

此时如果这台 LXC 在局域网里面,在另一台电脑上应该就已经可以开始 stream 游戏了。