One Gaming PC, Three People, Zero Patience
You own one decent gaming PC. Your household contains three people who all want to play at 8 PM. The usual answer is a fight over who gets the good seat, followed by a second PC nobody budgeted for.
There is a cheaper answer: run one Steam Headless v2 container per player on the same box. Each container is its own desktop with its own Steam, streamed to a laptop, a Steam Link or a phone with Moonlight. One GPU can serve more than one seat, and every extra GPU gives you more. Below you get the hardware math, a working compose file, our own measurements, and a long list of ways this goes sideways.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/gaming/steam-headless-one-pc-many-players
What a seat actually is
Steam Headless v2 (the josh5/steam-headless image, current tag 2.44.20261004.1 as of mid-October 2026) is a Fedora 44 container. Inside it runs a full KDE Plasma desktop on KWin Wayland, under systemd, with Steam and Sunshine built in. You stream it with Moonlight, or open the new web UI (SHUI) on port 8483 and stream from a browser over WebSocket or WebRTC.
Docs live at steamheadless.com. The GitHub README on master still describes v1 (Debian, Xfce, noVNC), so don’t follow it. The v1 line is the debian tag.
One container is one seat. Three containers are three people, each with their own desktop, their own Steam login and their own Sunshine. That is the whole mental model, and it is why this setup is easy to reason about at 2 AM.
Hardware planning: the per-seat budget
The docs budget 1.3 to 1.6 GiB of RAM per idle container, 2 GiB of shared memory, and 2 to 4 GiB of VRAM per seat once games are running. Our measurements (more below) landed at about 1.9 GiB RAM per idle container and about 300 MiB VRAM per idle seat. Games are what eat the rest, so plan on the docs’ number.
Some rules of thumb that follow:
- VRAM is the ceiling. An 8 GB card fits two or three seats if the games are light. Three seats running modern titles at the docs’ 4 GiB each will not fit.
- NVENC session limit. NVIDIA’s support matrix now lists 12 concurrent encode sessions for every GeForce from GTX 10 to RTX 50, laptop parts included. The old figures of 3 and 8 are outdated. Session count is not your bottleneck.
- Pin CPUs. On a Ryzen, keep each seat’s
cpusetinside one CCX so a game’s threads don’t hop across the chip. Pinning does not hide the core count (see the gotchas). - Extra GPUs are extra seats. Every GPU is its own render node. Map seats to GPUs by render node.
To see which renderD* belongs to which card:
ls -l /sys/class/drm/renderD*/devicels -l /dev/dri/by-pathEach line points at a PCI address. Match that to lspci and you know which node is which card. NVIDIA users map the renderD node only, never card*.
The compose file
The layout follows the official “multiple containers” page. Two or more seats need macvlan, one LAN IP each. Host networking gives you one container per host, and bridge networking is strictly unsupported because NAT breaks Moonlight discovery and WebRTC.
The file uses a YAML anchor for the shared base and three seats named parent, kid1 and kid2. Addresses assume a 192.168.50.0/24 LAN with the router’s DHCP pool below .192. Change them to match yours.
# Three Steam Headless v2 seats on one host, one LAN IP each (macvlan).# Export STEAM_HEADLESS_PASSWORD (12+ chars) before "docker compose up".x-seat: &seat image: josh5/steam-headless:2.44.20261004.1 restart: unless-stopped # NVIDIA only: uncomment these two lines and the two env vars below. # runtime: nvidia cgroup: private tty: true stop_signal: SIGRTMIN+3 stop_grace_period: 30s pids_limit: 8192 shm_size: 2gb cap_add: [DAC_READ_SEARCH, NET_ADMIN, SYS_ADMIN, SYS_NICE, SYS_PTRACE] security_opt: - seccomp=unconfined - apparmor:unconfined - systempaths=unconfined - writable-cgroups=true device_cgroup_rules: - "c 13:* rwm" tmpfs: - /run - /run/lock - /tmp - /dev/input:rw,dev,mode=0755 environment: &seat-env PUID: "1000" # same PUID/PGID in every seat: they share /mnt/games PGID: "1000" # TZ: Europe/London # optional; overrides the host localtime mount DISPLAY_MODE: wayland ENABLE_STEAM_SYSTEMD_SERVICE: "true" # NVIDIA_VISIBLE_DEVICES: all # NVIDIA_DRIVER_CAPABILITIES: all networks: [lan] secrets: - source: sh-password target: /steam-headless-provisioning-password uid: "0" gid: "0" mode: 0400
services: parent: <<: *seat container_name: sh-parent hostname: sh-parent cpuset: "0-3" # keep each seat inside one CCX/CCD environment: <<: *seat-env SUNSHINE_NAME: sh-parent devices: # per seat: a devices list in the anchor would be replaced, not merged - /dev/dri/renderD128:/dev/dri/renderD128 - /dev/fuse - /dev/uinput - /dev/uhid networks: lan: ipv4_address: 192.168.50.193 volumes: - parent-state:/var/lib/steam-headless - parent-journal:/var/log/journal - ./homes/parent:/home/steamheadless:Z # Steam login, settings, saves - /mnt/games:/mnt/games:z - /sys/devices/virtual/input:/sys/devices/virtual/input:ro - /etc/localtime:/run/host-localtime:ro
kid1: <<: *seat container_name: sh-kid1 hostname: sh-kid1 cpuset: "4-7" environment: <<: *seat-env SUNSHINE_NAME: sh-kid1 devices: - /dev/dri/renderD128:/dev/dri/renderD128 # same GPU: shared seat - /dev/fuse - /dev/uinput - /dev/uhid networks: lan: ipv4_address: 192.168.50.194 volumes: - kid1-state:/var/lib/steam-headless - kid1-journal:/var/log/journal - ./homes/kid1:/home/steamheadless:Z # Steam login, settings, saves - /mnt/games:/mnt/games:z - /sys/devices/virtual/input:/sys/devices/virtual/input:ro - /etc/localtime:/run/host-localtime:ro
kid2: <<: *seat container_name: sh-kid2 hostname: sh-kid2 cpuset: "8-11" environment: <<: *seat-env SUNSHINE_NAME: sh-kid2 devices: - /dev/dri/renderD129:/dev/dri/renderD129 # second GPU, if you have one - /dev/fuse - /dev/uinput - /dev/uhid networks: lan: ipv4_address: 192.168.50.195 volumes: - kid2-state:/var/lib/steam-headless - kid2-journal:/var/log/journal - ./homes/kid2:/home/steamheadless:Z # Steam login, settings, saves - /mnt/games:/mnt/games:z - /sys/devices/virtual/input:/sys/devices/virtual/input:ro - /etc/localtime:/run/host-localtime:ro
volumes: parent-state: parent-journal: kid1-state: kid1-journal: kid2-state: kid2-journal:
secrets: sh-password: environment: STEAM_HEADLESS_PASSWORD
networks: lan: driver: macvlan driver_opts: parent: eth0 macvlan_mode: bridge ipam: config: - subnet: 192.168.50.0/24 gateway: 192.168.50.1 ip_range: 192.168.50.192/27 # outside the router's DHCP pool aux_addresses: host-shim: 192.168.50.222 # reserved for the host shim belowA few things in there deserve a sentence each.
- Per-seat
devices. Each seat lists its own render node plus/dev/fuse,/dev/uinputand/dev/uhid. Adeviceslist in the anchor would be replaced, not merged, so the list lives on each seat. kid2usesrenderD129. That is the second-GPU example. If you have one GPU, point all three seats atrenderD128.- NVIDIA. Install the NVIDIA Container Toolkit, uncomment
runtime: nvidia,NVIDIA_VISIBLE_DEVICESandNVIDIA_DRIVER_CAPABILITIES(the docs useallfor both), and still map only the render node. Turing and newer should use the open kernel modules. Maxwell and Pascal need the 580xx branch. Don’t install graphics packages inside the container. - AMD and Intel. Map the render node and you are done.
- Homes. Each seat gets its own
./homes/<seat>bind for/home/steamheadless. That holds the Steam login, settings and saves, so recreating a container doesn’t log anyone out. - Games.
/mnt/gamesis shared. Steam Headless adds/mnt/games/GameLibrary/Steamas a library automatically. - Password. The secret provisions the permanent password for user
steamheadlesswithout a first-run click. Use 12 or more characters, with no:. It only applies when a seat’s state is created for the first time, so changing it later does not rotate the password. All three seats get the same one.uidandgidmust be quoted strings.
Host setup
Three things happen on the host before docker compose up.
fs.inotify.max_user_instances = 1024sudo sysctl --system
# macvlan shim so the host can reach its own seatssudo ip link add macvlan-shim link eth0 type macvlan mode bridgesudo ip addr add 192.168.50.222/32 dev macvlan-shimsudo ip link set macvlan-shim upsudo ip route add 192.168.50.192/27 dev macvlan-shim
# NVIDIA only: must print Ycat /sys/module/nvidia_drm/parameters/modesetThe example repo has a host-setup.sh that does all of this and can be re-run safely. The shim does not survive a reboot, so persist it with your network manager.
Then export the password and start the seats:
mkdir -p homes/parent homes/kid1 homes/kid2 # or Docker creates them as rootexport STEAM_HEADLESS_PASSWORD='something-12-chars-plus'docker compose up -dWe ran it, here are the numbers
We ran two Steam Headless v2 containers on one RTX 3070 Laptop (8 GB), NVIDIA driver 595.99.02, Docker 29.8.0, Compose v5.5.1. This was a smoke test with bridge networking and localhost ports, so it measures the containers, not the network layout above.
| Measurement | Result |
|---|---|
| Image size | 3.8 GB (pulled in 39 s) |
| First-start download per container | 423 MB NVIDIA installer, to build 32-bit driver libs (about 13 s here) |
| Time to Sunshine listening | 35 s with both starting together |
| Warm restart | 12 s |
| VRAM, no containers | 9 MiB |
| VRAM, both idle | 581 MiB |
| VRAM, both streaming | 1206 MiB |
| RAM per container, idle | about 1.9 GiB, 250 to 390 processes |
| Extra disk per container | about 2.7 GB home, 565 MB state volume, 580 MB writable layer |
| Concurrent encodes | 2 H.264 NVENC sessions, 1280x720 at about 30 fps |
| Encoder load | 1.3 to 1.6 ms average encode latency, 7% encoder utilization, 16% GPU utilization |
That works out to about 300 MiB of VRAM per idle seat and about 600 MiB per streaming seat, before any game loads. SHUI picked nvidia-nvenc on its own. h264_nvenc and hevc_nvenc worked. AV1 failed, which is expected on Ampere.
What we did not test: Moonlight pairing and streaming over macvlan, real games, and a Steam login. The streams in our test were two browser sessions on the SHUI page. Treat the macvlan section of this post as “follows the docs”, not “we proved it”.
Steam Headless vs Wolf
The other way to do this is Games on Whales and its Wolf server. Wolf’s last GitHub release tag is v2024.07, but it ships a rolling ghcr.io/games-on-whales/wolf:stable image (latest build 2026-09-29). It is one process on host networking that mounts the Docker socket and spawns an app container for each streaming session.
| Steam Headless v2 | Wolf | |
|---|---|---|
| Model | One persistent desktop per person | One server, a session container per stream |
| Idle cost | Every seat holds RAM and some VRAM all day | Only the Wolf server until someone connects |
| Networking | macvlan, one LAN IP per seat | One IP, one port set |
| Browser streaming | Yes (SHUI on 8483) | No, Moonlight only |
| Docker socket | Not needed | Required (effectively root on the host) |
| Co-op | Separate seats | “Lobbies”: several players join one app instance |
| Resolution and FPS | Follows the Moonlight client by default (Sunshine auto display mode) | Created per client on demand |
| Pairing | PIN or SHUI login | PIN entered at a URL Wolf prints in its logs |
| Config | Compose and env vars | config.toml and profiles |
Verdict: pick Steam Headless if each person wants their own persistent desktop with their own Steam client, installed games and settings, and you want a browser fallback for the machine you can’t install Moonlight on. It has no Docker socket exposure and a simpler mental model. You pay for it with a macvlan setup and idle RAM.
Pick Wolf if the box idles most of the day, you want one IP and no macvlan, or you want couch co-op where several people join the same game. You pay with the Docker socket, child containers you can configure less, and leftover containers plus log-scraping for pairing, which are common complaints.
For a household of three with fixed players, Steam Headless is the one we would run. If your household is “whoever is home”, look at Wolf.
The gotchas
This is the part to bookmark. Two of these bit us during the test. The rest come from the docs, Valve, and the project’s issue tracker.
Bridge or host networking cannot do two seats
Symptom: the second container fights over ports, or Moonlight can’t find the host. Cause: host networking allows one container per host. Bridge networking is strictly unsupported because NAT breaks Moonlight discovery and WebRTC. Each container also runs its own avahi on 5353/udp, and mDNS discovery works on macvlan but not behind NAT. Fix: macvlan, one LAN IP per seat, as in the compose file. Our test used bridge with localhost ports only to measure resources.
The host can’t reach its own seats
Symptom: you can reach 192.168.50.193 from your laptop but not from the host.
Cause: Docker macvlan containers cannot talk to the host directly.
Fix: the host shim from the setup section: a macvlan sub-interface on the same parent in mode bridge, with a route to the seat range. ipvlan l2 is an alternative. Your switch must allow multiple MAC addresses per port.
macvlan over Wi-Fi fails
Symptom: containers start, nothing answers. Cause: macvlan over Wi-Fi generally fails. Fix: put the host on wired Ethernet. The docs say the same.
Seat IPs colliding with DHCP
Symptom: a seat vanishes when a phone joins the network.
Cause: the router handed out the seat’s address.
Fix: use ip_range outside the DHCP pool, or reserve the addresses on the router. Reserve the shim address with aux_addresses, as the compose file does.
Sunshine refuses connections on 47990
Symptom: connection refused on port 47990 right after start.
Cause: Sunshine stays disabled until first-run setup finishes. The journal says: Sunshine remote access is disabled until Steam Headless setup is complete.
Fix: read the temporary password from docker logs (also at /var/lib/steam-headless/auth/bootstrap-password), log in to https://IP:8483 as steamheadless, and set a permanent password of 12 or more characters. Or skip all that with the provisioning secret.
Compose rejects the secret’s uid and gid
Symptom: services.desktop-a.secrets.0.gid must be a string.
Cause: the numbers are unquoted.
Fix: write uid: "0" and gid: "0". Also note that Compose v5.5.1 warns that uid, gid and mode on secrets are ignored outside Swarm mode. The file still validates.
The inotify limit is shared
Symptom: the third or fourth seat starts but apps fail oddly.
Cause: all containers run as UID 1000 and share one inotify pool. It cannot be set per container. We counted 48 instances under UID 1000 with just two idle desktops, and the host default for max_user_instances is 128.
Fix: 1024 for 2 to 3 seats, 3072 for 4 to 8, 8192 or more for 9 and up. Persist it in /etc/sysctl.d.
NVIDIA without nvidia_drm.modeset=1
Symptom: latency above 180 ms and logs full of DRM_IOCTL_MODE_CREATE_DUMB failed.
Cause: KWin falls back to software rendering (kms_swrast).
Fix: set nvidia_drm.modeset=1 on the kernel command line and check /sys/module/nvidia_drm/parameters/modeset prints Y. It is mandatory.
Steam Families: one copy, one player
Symptom: two people want the same shared game and the second can’t launch it. Cause: per Valve’s 2024 announcement, a family holds up to 6 members and one copy is one player at a time. Members can play different games from the shared library simultaneously. Fix: buy a second copy for the games two people want together. Know the rules before you build the household around it. An adult who leaves waits 1 year from joining the previous family before joining or creating another, and the vacated slot has a 1-year cooldown. If a member earns a cheating ban playing your copy, you get banned in that game too. Developers can also opt games out of sharing.
A shared library and simultaneous installs
Symptom: corrupted game files after two seats update at once.
Cause: Steam does not support several containers changing the same game files at once.
Fix: stop the other containers before installing, updating, verifying, moving or deleting games. Every seat needs the same PUID and PGID so the files stay readable. Mixing owners breaks the shared library fast.
First start costs disk and bandwidth, per seat
Symptom: the first up takes minutes and eats disk.
Cause: on NVIDIA, each container downloads its own 423 MB installer (to build 32-bit driver libs). Every seat also carries about 2.7 GB of home, 565 MB of state and 580 MB of writable layer on top of the 3.8 GB image (the image is shared between seats).
Fix: budget about 4 GB per seat before games (most of it lands in that seat’s home folder), and expect a 12 s warm restart after that.
Anti-cheat
Symptom: a multiplayer title refuses to launch or kicks you. Cause: Easy Anti-Cheat titles often fail, and it depends on the title and publisher. areweanticheatyet.com tracked 1,167 games when we checked: 196 Supported, 278 Running, 638 Broken, 53 Denied. Fix: check the specific game there before you tell a kid it will work.
AV1 is not available on every card
Symptom: av1_nvenc fails with Provided device doesn't support required NVENC features.
Cause: Ampere has no AV1 encode. The Navi 12 chips on the Radeon Pro V520 and V540 (VCN 2.0) encode H.264 and HEVC only.
Fix: use H.264 or HEVC. Both worked in our test.
Sunshine auto-config overwrites your edits
Symptom: your manual encoder or app changes in Sunshine vanish after a restart.
Cause: SUNSHINE_ENABLE_AUTO_CONFIG=true rewrites them on every start.
Fix: set it to false once you tune Sunshine by hand.
Display settings only apply to a fresh profile
Symptom: you changed INITIAL_DISPLAY_WIDTH, HEIGHT or REFRESH and nothing happened.
Cause: the defaults (1920, 1080, 120) only apply when the profile is created.
Fix: set them before the first start, or change the resolution in the desktop afterwards. Moonlight clients still get their requested mode, because Sunshine’s auto display mode switches it per stream.
cpuset doesn’t hide the core count
Symptom: a game crashes on launch on a many-core CPU. Cause: some games crash under Proton on CPUs with many cores, and a Docker cpuset does not hide the host’s core count (per Wolf’s docs on environment considerations). Fix: none that we verified. Check that title’s Proton reports before blaming the container.
An idle seat pulling real power
Symptom: after a stream ends, an AMD GPU sits at 33 to 71 W instead of about 8 W. Cause: the stream’s cleanup leaves Steam on an animated page, which keeps the GPU busy (GitHub issue #269). Fix: park each seat’s Steam on a static page when nobody is using it.
Steam offers to stream from the other seat
Symptom: on a seat logged into the same account, or one sharing a family library, the Play button turns into Connect or Stream.
Cause: in macvlan bridge mode, the Steam clients on sibling seats discover each other on the LAN.
Fix: set macvlan_mode: private on the network. That blocks the host shim, so you lose host-to-seat access in exchange.
Log noise you can ignore
We saw all of these on healthy seats:
EGL: context priority set to HIGH but CAP_SYS_NICE capability is missing, even withSYS_NICEset- A transient
KWin compositor has no open DRM render deviceat startup - Steam listing
llvmpipeas an extra Vulkan GPU - A “marked world-writable” config warning
- An
xdg-desktop-portalPIDFD ioctl warning
Search your logs for DRM_IOCTL_MODE_CREATE_DUMB failed instead. That one matters.
Other open v2 issues
At the time of writing: DLSS and Frame Generation go missing after a v1 to v2 migration (#268), Steam Remote Play on AMD falls back to libx264 (#263), and SteamVR doesn’t work (#266).
Used datacenter GPUs
A cheap retired datacenter card is the tempting way to add seats. The AMD Radeon Pro V540 (PCI ID 1002:7362) carries two Navi 12 GPUs with 8 GB HBM2 each, and mainline amdgpu supports it. Each GPU shows up as its own render node, so one card can be two seats. The single-GPU V520, the card AWS used in its G4ad instances, gives you one seat.
The caveats we could verify: H.264 and HEVC only, no AV1. AMD told one V540 buyer the card was never released to the market. Some V540 owners report only one of the two GPUs working. For cooling, power and display outputs, check the specific card before you buy. Datacenter cards are often passively cooled and expect server airflow.
Security posture
A seat needs SYS_ADMIN, seccomp=unconfined, apparmor:unconfined, and friends. The docs say never to replace these with privileged: true. This is a trusted-LAN box. Do not forward 8483 or the Sunshine ports (TCP 47984, 47989, 47990, 48010, UDP 47998, 47999, 48000, 48002, 48010) to the internet. For remote play, put the host behind a VPN such as Tailscale or WireGuard.
The SumGuy Take
Hiring a forklift to move a couch is overkill. Buying a second gaming PC so your kid can play at the same time as you is the same mistake with a bigger invoice. One box, a few containers and a macvlan network does the job, and your 2 AM self will thank you for the fact that each seat is just a container you can restart.
Start with two seats on the GPU you own. Measure VRAM with a real game running. Add a seat or a card only when the numbers say so.
Common Questions
Can two people play the same Steam game on one Steam Families copy?
No. One copy means one player at a time. Two people can play the same game simultaneously only if the family owns two copies. Family members can play different games from the shared library at the same time, so three seats work fine with three different titles.
How many Steam Headless containers can one GPU run?
The limit is VRAM, not encoding. NVIDIA’s matrix lists 12 concurrent NVENC sessions on GeForce cards. The docs budget 2 to 4 GiB of VRAM per seat with games running, so an 8 GB card realistically fits two seats and three with light games.
Does Steam Headless work with an NVIDIA GPU that has no monitor attached?
Yes. Steam Headless v2 renders through a render node and streams the result, so no display is needed. You do need the NVIDIA Container Toolkit, runtime: nvidia, and nvidia_drm.modeset=1. Without modeset, KWin falls back to software rendering and latency goes above 180 ms.
Can I use Steam Remote Play instead of Moonlight with Steam Headless?
We did not test it. An open issue (#263) reports Steam Remote Play on AMD falling back to libx264 because a 32-bit VA driver is missing. Moonlight with the built-in Sunshine, or the SHUI browser stream on port 8483, are the paths the docs describe and the ones we ran.