Skip to content
Go back

One PC, Many Players: Steam Headless

By KingPin 19 min read
One PC, Many Players: Steam Headless
Contents

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:

To see which renderD* belongs to which card:

Terminal window
ls -l /sys/class/drm/renderD*/device
ls -l /dev/dri/by-path

Each 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.

compose.yaml
# 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 below

A few things in there deserve a sentence each.

Host setup

Three things happen on the host before docker compose up.

/etc/sysctl.d/99-steam-headless.conf
fs.inotify.max_user_instances = 1024
Terminal window
sudo sysctl --system
# macvlan shim so the host can reach its own seats
sudo ip link add macvlan-shim link eth0 type macvlan mode bridge
sudo ip addr add 192.168.50.222/32 dev macvlan-shim
sudo ip link set macvlan-shim up
sudo ip route add 192.168.50.192/27 dev macvlan-shim
# NVIDIA only: must print Y
cat /sys/module/nvidia_drm/parameters/modeset

The 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:

Terminal window
mkdir -p homes/parent homes/kid1 homes/kid2 # or Docker creates them as root
export STEAM_HEADLESS_PASSWORD='something-12-chars-plus'
docker compose up -d

We 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.

MeasurementResult
Image size3.8 GB (pulled in 39 s)
First-start download per container423 MB NVIDIA installer, to build 32-bit driver libs (about 13 s here)
Time to Sunshine listening35 s with both starting together
Warm restart12 s
VRAM, no containers9 MiB
VRAM, both idle581 MiB
VRAM, both streaming1206 MiB
RAM per container, idleabout 1.9 GiB, 250 to 390 processes
Extra disk per containerabout 2.7 GB home, 565 MB state volume, 580 MB writable layer
Concurrent encodes2 H.264 NVENC sessions, 1280x720 at about 30 fps
Encoder load1.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 v2Wolf
ModelOne persistent desktop per personOne server, a session container per stream
Idle costEvery seat holds RAM and some VRAM all dayOnly the Wolf server until someone connects
Networkingmacvlan, one LAN IP per seatOne IP, one port set
Browser streamingYes (SHUI on 8483)No, Moonlight only
Docker socketNot neededRequired (effectively root on the host)
Co-opSeparate seats“Lobbies”: several players join one app instance
Resolution and FPSFollows the Moonlight client by default (Sunshine auto display mode)Created per client on demand
PairingPIN or SHUI loginPIN entered at a URL Wolf prints in its logs
ConfigCompose and env varsconfig.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:

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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Idle Watts Audit: Halve Your Lab's Draw
Next Post
Database Connection Storms, Explained

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts