Share one Steam game library across multiple Linux users with fully isolated Proton prefixes. Uses bubblewrap to create a per-user kernel overlay on /opt/steam/steamapps/compatdata/ so game files stay shared while Proton prefixes are isolated per user, with no compatibility tool selection or per-game configuration required. Includes: - steam-shared launcher that sets up the per-user overlay and execs Steam inside a bwrap mount namespace - activate/uninstall scripts plus an add-user helper for steamshare group membership - permission watcher (steam-fix-perms.path/.service) to keep ACLs correct under pressure-vessel's restrictive mode bits - .desktop override that routes the system Steam launcher through steam-shared - Nix flake exposing activate, uninstall, and add-user packages - design doc and implementation plan covering the approach
9.4 KiB
Steam Shared Library: Bubblewrap Overlay Approach
Date: 2026-04-04 Status: Approved
Problem
Multiple users on a shared Linux machine need to play games from a single Steam library (/opt/steam/) without Proton prefix ownership conflicts. Steam mixes shared state (game files in common/) with per-user state (Wine prefixes in compatdata/) in the same directory. Every Proton variant writes to compatdata/ — and when two users share the same prefix, Proton/Wine fails with ownership errors (wineserver: pfx is not owned by you).
The previous approach (a Proton compatibility tool wrapper that redirected STEAM_COMPAT_DATA_PATH) was brittle: it required manual per-game selection, any per-game Proton override bypassed it, and Steam sometimes wrote to the original path before the wrapper ran.
Solution
Use bubblewrap (bwrap) to mount a kernel overlay on /opt/steam/steamapps/compatdata/ per user. Each user launches Steam through a wrapper script that creates a private mount namespace via bwrap. Writes to compatdata go to a per-user upper layer directory. Reads fall through to the shared lower layer. Game files (common/, shadercache/, etc.) are not overlaid — they remain shared and writable by all steamshare group members.
This is transparent to Steam and every Proton variant. No compatibility tool selection, no environment variable hacks, no per-game configuration.
How It Works
/opt/steam/steamapps/
├── common/ ← shared game files (real filesystem, group writable)
├── shadercache/ ← shared shader cache (real filesystem)
├── compatdata/ ← OVERLAID per-user via bwrap
│ ├── <appid>/ ← lower layer: shared (mostly empty dirs Steam creates)
│ └── ... ← upper layer: ~/.local/share/steam-shared/upper/
└── ...
Per user (inside bwrap mount namespace):
/opt/steam/steamapps/compatdata/ ← overlay mount
lower: /opt/steam/steamapps/compatdata/ (shared, read-through)
upper: ~/.local/share/steam-shared/upper/ (per-user writes)
work: ~/.local/share/steam-shared/work/ (overlayfs workdir)
When a user launches a Proton game:
- Steam sets
STEAM_COMPAT_DATA_PATH=/opt/steam/steamapps/compatdata/<appid> - Proton creates/opens the prefix at that path
- The kernel overlay redirects the write to
~/.local/share/steam-shared/upper/<appid>/ - Other users are unaffected — they have their own mount namespace with their own upper layer
Target Platforms
- Linux (Arch/CachyOS) with native Steam — primary target
- Linux with Flatpak Steam — supported via
--filesystem=/opt/steamoverride - NixOS — future, via NixOS module output from the same flake
Prerequisites
bubblewrap(already installed as Flatpak dependency on most systems)aclpackage forsetfacl- Kernel overlay support (standard on all modern kernels)
- Native Steam (
pacman -S steam) or Flatpak Steam
Project Structure
steam-shared-library/
├── flake.nix ← Nix flake: outputs activate + uninstall packages
├── flake.lock
├── scripts/
│ ├── activate.sh ← system setup (requires sudo)
│ ├── uninstall.sh ← reversal (requires sudo)
│ └── steam-shared.sh ← per-user Steam launcher
├── desktop/
│ └── steam-shared.desktop ← .desktop override for Steam
├── README.md
└── docs/
└── specs/
└── 2026-04-04-bwrap-overlay-design.md
Components
1. Activation Script (nix run .#activate)
Requires sudo. Idempotent — safe to re-run.
Actions:
- Create
steamsharegroup if it doesn't exist - Create
/opt/steam/steamapps/compatdata/with setgid2775and group ACLs forsteamshare - Wipe all contents of
/opt/steam/steamapps/compatdata/*/(clean slate — stale shared prefixes) - Install
steam-sharedlauncher script to/usr/local/bin/steam-shared - Install
steam-shared.desktopto/usr/local/share/applications/(shadows systemsteam.desktop) - Verify
bubblewrapis installed (error if not)
Does NOT:
- Add users to the steamshare group (manual:
sudo usermod -aG steamshare <user>) - Install Steam itself
- Install a permission watcher (ACLs + setgid should suffice)
2. Steam Launcher Script (/usr/local/bin/steam-shared)
Runs as the logged-in user. No root required.
#!/bin/bash
set -euo pipefail
OVERLAY_DIR="$HOME/.local/share/steam-shared"
SHARED_COMPATDATA="/opt/steam/steamapps/compatdata"
mkdir -p "$OVERLAY_DIR/upper" "$OVERLAY_DIR/work"
# Detect Steam installation: prefer native over Flatpak
STEAM_CMD=()
if [ -x /usr/bin/steam ]; then
STEAM_CMD=(/usr/bin/steam)
elif flatpak info com.valvesoftware.Steam &>/dev/null; then
flatpak override --user --filesystem=/opt/steam com.valvesoftware.Steam 2>/dev/null
STEAM_CMD=(flatpak run com.valvesoftware.Steam)
else
echo "steam-shared: no Steam installation found (checked /usr/bin/steam and Flatpak)" >&2
exit 1
fi
exec bwrap \
--dev-bind / / \
--overlay-src "$SHARED_COMPATDATA" \
--overlay "$OVERLAY_DIR/upper" "$OVERLAY_DIR/work" "$SHARED_COMPATDATA" \
-- "${STEAM_CMD[@]}" "$@"
3. Desktop File Override (/usr/local/share/applications/steam-shared.desktop)
Shadows the system-installed steam.desktop by using the same Name and higher-priority location. Uses the same icon and categories so it looks identical in the app menu.
Key fields:
Exec=steam-shared %UName=SteamIcon=steam(uses system Steam icon)
4. Uninstall Script (nix run .#uninstall)
Requires sudo. Reverses everything the activation script does:
- Remove
/usr/local/bin/steam-shared - Remove
/usr/local/share/applications/steam-shared.desktop - Remove
steamsharegroup (and membership) - Print instructions for optional manual cleanup (
/opt/steam/, per-user~/.local/share/steam-shared/)
Does NOT remove Steam itself or per-user save data.
5. Nix Flake
Outputs:
packages.<system>.activate— activation script (wrapsscripts/activate.sh)packages.<system>.uninstall— uninstall script (wrapsscripts/uninstall.sh)- Future:
nixosModules.default— native NixOS module for declarative system config
Steam Detection Logic
The launcher prefers native Steam over Flatpak Steam:
- If
/usr/bin/steamexists → use native - Else if
flatpak info com.valvesoftware.Steamsucceeds → use Flatpak with--filesystem=/opt/steam - Else → error
If both are installed, native wins. This is documented in the README — having both installed is discouraged because it leads to two separate Steam configs, library confusion, and double disk usage.
Flatpak Steam Specifics
Flatpak Steam runs inside its own sandbox. For the bwrap overlay to work:
- The Flatpak app needs filesystem access to
/opt/steam/(granted viaflatpak override --user --filesystem=/opt/steam) - The bwrap overlay is set up OUTSIDE Flatpak's sandbox — bwrap creates the mount namespace first, then
flatpak runinherits it - Confirmed working: writes inside Flatpak's sandbox go through bwrap's overlay to the per-user upper layer
Integration with Dotfiles
The dotfiles project (~/.syncthing/dotfiles/) integrates this via:
nix run github:felixfoertsch/steam-shared-library#activateruns on Linux machines- Old wrapper system cleanup is handled by
roles/legacy-cleanup/in the Ansible playbook - Users are added to
steamsharegroup via themanaged-usersrole's create flow
Error Handling
- bubblewrap not installed: activation script checks and fails with clear error message
- Steam not installed: launcher script checks both native and Flatpak, fails with clear message
- User not in steamshare group: Steam launches but can't access
/opt/steam/— standard permission denied from filesystem - Stale work dir: overlayfs work directories can become stale (causes "Device or resource busy"). The launcher creates fresh dirs if the work dir contains a stale
work/subdirectory. - /opt/steam/ doesn't exist: activation script creates it; launcher checks and fails with clear message
User Workflow
- Admin runs
nix run .#activate - Admin adds users to steamshare group:
sudo usermod -aG steamshare <user> - User logs out and back in (for group membership)
- User launches Steam normally via app menu or command line
- Games install to
/opt/steam/— visible to all users - Proton prefixes go to
~/.local/share/steam-shared/upper/<appid>/— per-user, isolated - Multiple users can be logged in simultaneously (GDM user switching) — each has their own mount namespace
Guided Gates
- GG-1: Run
nix run .#activate, verify group/dir/ACLs/desktop file/launcher script created correctly - GG-2: Launch Steam via the desktop entry, verify overlay is active (write to compatdata goes to upper layer)
- GG-3: Install a game from Steam, verify it lands in
/opt/steam/steamapps/common/(shared, not overlaid) - GG-4: Launch a Proton game with any Proton variant, verify prefix lands in
~/.local/share/steam-shared/upper/<appid>/and NOT in/opt/steam/steamapps/compatdata/<appid>/ - GG-5: Create a second user, add to steamshare, launch Steam, verify shared games visible and prefix is isolated
- GG-6: Both users logged in via GDM simultaneously — both can access Steam without conflicts
- GG-7: Run
nix run .#uninstall, verify clean removal of all artifacts