Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

This is currently WIP. Descriptions and details how to do something are undergoing. If you want to start right away, the textblocks here might as well be explained by a chatbot. I will add a description from my side in the near future.

Containerized, Headless Sunshine – How-To

This repository documents a personal but production-tested setup for running Sunshine in a headless, containerized Wayland environment, without relying on HDMI dummy plugs or X11-based display emulation.

1. Motivation

Most Sunshine setups assume one of the following:

  • a physical display is attached, or
  • a dummy HDMI plug is used to fake one, or
  • X11 is available and acceptable

On modern Linux systems, especially those that are Wayland-first or run in server-like environments, these assumptions can be limiting.

The goal of this setup was to:

  • Run Sunshine fully headless
  • Avoid dummy plugs, virtual monitors, and display hacks
  • Keep the host system clean, with Sunshine isolated in a container
  • Use native Wayland, not X11
  • Maintain reliable input handling across the container boundary
  • Work on a stock Ubuntu 24.04 system with minimal host modifications

To achieve this, Sunshine is executed inside a systemd-nspawn container with GPU access, while input events are forwarded using vuinputd. The Wayland session lives entirely inside the container and does not depend on a physical display being present on the host.

This repository does not aim to be the “recommended” or “official” way to run Sunshine. Instead, it documents what worked, why certain decisions were made, and which pitfalls were encountered along the way, in the hope that it may help others with similar constraints or curiosity.

2. Prerequisites / Assumptions

  • In this guide, I assume Ubuntu 24.04 is used, but should work for other systemd distributions very similarly
  • GPU supports Wayland; NVIDIA is untested
  • This is not Docker; systemd-nspawn is used
  • Python 3 is installed

3. Step-by-step guide

3.1 Prepare the host

Install vuinputd

Exposing the host’s /dev/uinput directly into a container breaks isolation:

  • Containers can create devices visible system-wide or to other containers.
  • Keyboards and mice may attach to host seats or inject input into active host sessions.

vuinputd exposes a virtual /dev/uinput device inside containers (via CUSE). Input devices created by containerized apps are forwarded to the host kernel’s uinput subsystem, where they appear as normal /dev/input/event* devices visible to all host applications. Those devices are then injected into the containers with udev announcements.

git clone https://github.com/joleuger/vuinputd.git
cd vuinputd
apt-get install build-essential libc6-dev libfuse3-dev pkg-config fuse3 libudev-dev libclang-dev
cargo build --release

cp target/release/vuinputd /usr/local/bin
cp vuinputd/udev/90-vuinputd-protect.rules /etc/udev/rules.d
cp vuinputd/udev/90-vuinputd.hwdb /etc/udev/rules.d/hwdb.d/
cp vuinputd/systemd/vuinputd.service /etc/systemd/system/
systemd-hwdb update
udevadm control --reload
systemctl daemon-reload
systemctl enable --now vuinputd

Ensure VT is no problem

Check the output of vuinputd with journalctl -u vuinputd -S -5min. If there is a warning like tty1 keyboard mode is active (mode=2) — VT may consume input an active keyboard that was created via vuinputd might create phantom input on tty1. This can be suppressed by starting a graphical session, using kmscon, or with vuinputd --vt-guard. This disables all keyboard input on the virtual terminals, including physical keyboards. Loss of local access may require recovery via SSH or a rescue boot.

Currently fallbackdm is under development, which registering a proper greeter session with systemd-logind, which suppresses the VT output without starting X11 or Wayland.

Install / enable systemd-machined and other systemd container tools.

apt-get install systemd-container

3.2 Install mkosi

We use mkosi (Make Operating System Image) to build a clean, reproducible container image without polluting the host.

git clone https://github.com/systemd/mkosi

3.3 Build the Ubuntu container image

  • Use mkosi to create Ubuntu 25.10 (questing) or 24.04 (noble)
  • Where the image ends up
  • What is intentionally not customized yet
PYTHONPATH=mkosi python3 -m mkosi --distribution=ubuntu --release=questing -t directory --output-directory=sunshine-server --force --with-network

3.4 Create the systemd-nspawn configuration

Direct start with systemd-nspawn

/usr/bin/systemd-nspawn -M sunshineserver -D /var/lib/machines/sunshineserver --network-veth --bind=/dev/vuinput:/dev/uinput --bind=/dev/dri --property="DeviceAllow=char-drm rw" --property="DeviceAllow=char-input rw" --property="DeviceAllow=/dev/vuinput rw" -b 

Create /etc/systemd/system/sunshineserver.service:

[Unit]
Description=Sunshine Headless Container
Documentation=man:systemd-nspawn(1)
PartOf=machines.target
Before=machines.target
After=network.target

[Service]
# The container type is "notify" so systemd knows when boot is done
Type=notify
Restart=on-failure
KillMode=mixed

# --- CONTAINER STARTUP ---
ExecStart=/usr/bin/systemd-nspawn -M sunshineserver -D /var/lib/machines/sunshineserver --network-veth --bind=/dev/vuinput:/dev/uinput --bind=/dev/dri --property="DeviceAllow=char-drm rw" --property="DeviceAllow=char-input rw" --property="DeviceAllow=/dev/vuinput rw" -b 

[Install]
WantedBy=multi-user.target
  • Device passthrough
  • Bind mounts
  • Networking
  • GPU access

A note on NVIDIA: Using and NVIDIA card might be possible. Analogue to the Games on Whales documentation, binding the following devices might be needed - /dev/nvidia-uvm - /dev/nvidia-uvm-tools - /dev/nvidia-caps/nvidia-cap1 - /dev/nvidia-caps/nvidia-cap2 - /dev/nvidiactl - /dev/nvidia0 - /dev/nvidia-modeset The device driver can be downloaded from for example from https://www.nvidia.com/en-eu/drivers/details/259055/ and needs to be installed both on the host and in the container. In the container, the flags ./NVIDIA-Linux-x86_64-$NV_VERSION.run --silent -z --skip-depmod --skip-module-unload --no-nvidia-modprobe --no-kernel-modules --no-kernel-module-source are required, because kernel modules make no sense inside the container. See also Games on Whales NVIDIA Dockerfile. This is completely untested, because I have no NVIDIA card. Comments and PRs in Github are welcome.

3.5 Start the container and enter it as root

  • systemctl daemon-reload
  • systemctl enable --now sunshineserver
  • machinectl shell sunshineserver
apt-get update
apt-get install -qy iputils-ping
apt-get remove -qy makedev
apt-get install -qy vim
apt-get install locales
  • Setup networking inside the container
  • timezone
  • Verify basic assumptions as ping

3.6 Validate input works


  • input test

3.7 Install runtime dependencies inside the container

Group these logically:

  • Wayland compositor: labwc
  • Media stack: pipewire
  • Sunshine
  • Optional: Flatpak + Heroic
apt install -yy libcairo2-dev libpango1.0-dev libxml2-dev libglib2.0-dev meson
apt install -yy build-essential cmake libwayland-dev wayland-protocols \
 libegl1-mesa-dev libgles2-mesa-dev libdrm-dev libgbm-dev libinput-dev \
 libxkbcommon-dev libudev-dev libpixman-1-dev libsystemd-dev libcap-dev \
 libxcb1-dev libxcb-composite0-dev libxcb-xfixes0-dev libxcb-xinput-dev \
 libxcb-image0-dev libxcb-render-util0-dev libx11-xcb-dev libxcb-icccm4-dev \
 freerdp2-dev libwinpr2-dev libpng-dev libavutil-dev libavcodec-dev \
 libavformat-dev universal-ctags
apt install -yy libxcb-util0-dev hwdata
# xwayland and vulkan
apt install -yy xwayland libvulkan-dev libxcb-ewmh-dev libliftoff0 vulkan-tools xwayland-run glslang-dev glslang-tools spirv-tools
# seat
apt install -yy polkitd pkexec policykit-1
apt install -yy foot libdisplay-info1 libfcft4t64 libutf8proc3 libxcb-res0 ncurses-term

apt install -yy  meson libdrm-dev libxkbcommon-dev libwlroots-dev libjansson-dev \
	libpam0g-dev libgnutls28-dev libavfilter-dev libavcodec-dev \
	libavutil-dev libturbojpeg0-dev scdoc
apt install -yy mesa-utils
apt-get install -yy flatpak
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install flathub com.heroicgameslauncher.hgl


apt install -y libcap2 libcurl4 libdrm2 libevdev2 libnuma1 libopus0 libpulse0 libva2 libva-drm2 libwayland-client0 libx11-6 miniupnpc libssl3 libayatana-appindicator3-1 libnotify4 libayatana-appindicator3-1 libayatana-ido3-0.4-0 libayatana-indicator3-7 libdbusmenu-glib4 libdbusmenu-gtk3-4 libminiupnpc17 libnotify4 miniupnpc
apt install -y udev


apt-get install pipewire-audio pulseaudio-utils


wget https://github.com/LizardByte/Sunshine/releases/download/v2025.628.4510/sunshine-ubuntu-24.04-amd64.deb
dpkg --install sunshine-ubuntu-24.04-amd64.deb

3.8 Fix rendering and libinput

/etc/systemd/system/fake-udev.service

[Unit]
Description=Fake udev directories
After=basic.target

[Service]
Type=one-shot

# Create directories for udev to ensure that libinput is listening on the netlink udev interface
ExecStartPre=/bin/mkdir -p /run/udev/data
ExecStartPre=/usr/bin/touch /run/udev/control
ExecStart=/usr/bin/echo file /run/udev/control is setup

[Install]
WantedBy=multi-user.target
systemctl enable fake-udev
gid_render=$(stat -c "%g" /dev/dri/renderD128)
groupadd -g $gid_render render

gid_video=$(stat -c "%g" /dev/dri/card0)
groupadd -g $gid_video video

Alternative setup_dri_groups.sh

chmod +x /usr/local/sbin/setup_dri_groups.sh
/usr/local/sbin/setup_dri_groups.sh

3.8 Configure user session & lingering

  • Create user
  • loginctl enable-linger
  • Why lingering matters for headless Wayland
  • Enable services

headless-labwc.service

[Unit]
Description=Headless labwc
After=basic.target
Requires=dbus.socket
Wants=dbus.socket

[Service]
Type=notify
NotifyAccess=all
Environment=WLR_BACKENDS=headless,libinput
Environment=WLR_LIBINPUT_NO_DEVICES=1
Environment=LIBSEAT_BACKEND=noop
Environment=LABWC_UPDATE_ACTIVATION_ENV=1
Environment=WLR_SCENE_DISABLE_DIRECT_SCANOUT=0
Environment=WLR_NO_HARDWARE_CURSORS=1
#Environment=LABWC_DEBUG_LIBSFDO=1
#ExecStartPre=/bin/sleep 3
ExecStart=/usr/local/bin/labwc -V --debug

[Install]
WantedBy=default.target

/home/user/.config/labwc/autostart

echo send READY=1 to systemd
systemd-notify READY=1

/home/user/.config/systemd/user/sunshine.service

[Unit]
Description=Sunshine
After=basic.target headless-labwc.service
Requires=headless-labwc.service

[Service]
Type=simple
Environment=WAYLAND_DISPLAY=wayland-0
#ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/sunshine

[Install]
WantedBy=default.target

/home/user/.config/systemd/user/wayvnc.service

[Unit]
Description=Wayvnc
After=basic.target
Requires=headless-labwc.service

[Service]
Type=simple
Environment=WAYLAND_DISPLAY=wayland-0
ExecStartPre=/bin/sleep 3
ExecStart=/usr/local/bin/wayvnc 0.0.0.0 -v

[Install]
WantedBy=default.target
adduser streamuser --disabled-password --gecos ""

loginctl enable-linger streamuser
#https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html

# -a -> append (otherwise the user’s other groups would be overwritten).
# -G -> specify the group(s).
usermod -aG video streamuser
usermod -aG render streamuser

systemctl --user enable headless-labwc.service
systemctl --user enable sunshine.service
#systemctl --user enable wayvnc.service

systemctl --user list-dependencies
# pipewire-pulse and dbus should be enlisted, otherwise also enable them for the user

3.9 First startup & verification

  • Start Wayland session
  • Start Sunshine
  • Start moonlight
  • Pair

3.10 Configure user desktop

  • Verify:
    • video
    • input
    • audio
    • HeroicGamesLauncher

4. Next steps

  • Waybar
  • Wayvnc
  • Menu configuration
cd ~
git clone https://github.com/labwc/labwc-menu-generator.git
cd labwc-menu-generator
meson setup build/
meson compile -C build/
meson install -C build/

mkdir -p ~/.config/labwc
labwc-menu-generator > ~/.config/labwc/menu.xml

Steam

# https://repo.steampowered.com/steam/
cd ~
apt-get install -yy steam-libs
apt-get install -yy distro-info-data lsb-release lsof python-apt-common python3-apt xbitmaps xterm
wget https://cdn.fastly.steamstatic.com/client/installer/steam.deb
apt-get install libglx-mesa0:i386
dpkg --install steam.deb
/usr/bin/steamdeps

5. Cleanup / Reset

  • How to stop/remove the container
  • How to undo host changes

About

Community guide for running Sunshine headlessly on Wayland in a container, without HDMI dummy plugs. Based on a systemd-nspawn setup on Ubuntu 24.04 with GPU access and vuinputd.

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages