Help: Shared local SteamLibrary, multi-user setup

Hi, I’m attempting to make a multi-user setup in order to separate our save games, browsing history, etc as much as possible.

Let’s say I have three users
-user1 (admin)
-user2 (user)
-user3 (user)

I created a group named steam
(sudo groupadd steam)
Then I added the users to the group
(sudo usermod -aG user1, etc)
Then to minimize disk usage for shared games, I created a directory for the common SteamLibrary
(sudo mkdir -p /var/games/SteamLibrary)
I then changed the owner group to steam
(sudo chgrp steam /var/games/SteamLibrary)
and also granted read-write-execute permission to the group
(sudo chmod 2775 /var/games/SteamLibrary)
To cover my base, I also added ACLs to grant write access to the steam group
(sudo setfacl -d -m g:steam:rwx /var/games/SteamLibrary)

I then setup the kids Steam account in their respective user profiles, and added the shared SteamLibrary in their Storage.

Steam sees the games, and all seems good.

However, when I try to start a game from the shared SteamLibrary, the games seems to fail silently.

Is there a guide on how to accomplish that kind of setup, or best practices to follow?

I’d really like to avoid having multi-GB games copies present in each user’s home directory, and keep our browser history, game saves, desktop environment setup as separate as possible.

Thanks!

Quick status update, so after fiddling a bit more and not finding a simple solution, I decided to have a single OS account for games (Steam, Heroic Launcher) and leveral the Steam switch user feature and have it autostart in Big Picture mode.

I use a non-gaming OS account for casual stuff to keep my browsing history, etc separate and locked away from others. That way my kids can login to that gaming session without me worrying about them messing up with the settings.

I have been fighting with this exact same thing. I’d like to have a shared Steam library for all users on my PC. The problem is Steam is moving the game’s Wine(Proton) prefix with the game files into the shared folder. The reason seems to be Steam seeing any added library as an external drive when an immutable file system is in use and hence moves the prefix with game files. Prefixes are per users and you can’t run a game with another user’s prefix. If you delete the prefix from shared library and let Steam re-create it for another user, then the game works for that other user, but no one else.

I haven’t actually gotten to try this on a distro with un-jailed filesystem so I could create a dedicated library folder somewhere else than /var, but probably will someday when I’m bored again. If it works it would definitely be a feature suggestion for Bazzite to have a dedicated, shared library folder somewhere.

I tried with CachyOS and open filesystem and exactly the same thing happened. Prefix wont stay in /home.

Omg I actually updated the docs with a PR about this yesterday, they seemed to think it wouldn’t be a common issue :stuck_out_tongue:

One of the things it could be is WINE

Basically you will just want to create an additional Steam library folder for the other users involved, and create a symlink for things like common (maybe shadercache too, and similarly copy or link any appmanifests.. just not temp or compatdata or workshop I think). Then add that new folder as the steam library for the additional user. The symlink of common will work and allow access to common game data, appmanifests make it show up in your steam list, and WINE gets to create new prefixes in a new compatdata folder for the new user.

I don’t know if this fix creates any additional quirks, but it’s been successful for getting games to launch on my other user account on my laptop

Thank you, I’ll have a look into it!

Thank you, I will try this.

I don’t see this as a WINE issue, but Steam being an idiot about handling prefixes. Other launchers have no problems with multiple users and prefixes. I will still try this, but don’t much like handling appmanifest files manually.

Disclaimer: The investigation, solution and this post was done in a collaboration with Claude.

We have a family gaming PC (Bazzite, but this applies to any Linux) with several local user accounts, each with their own Steam account. We wanted every game installed once, playable by everyone — without breaking Proton. Getting there took some debugging, so here’s the full working recipe including the pitfalls we hit.

The short version: game files can be shared through a common Steam library folder with group permissions + ACLs. Proton prefixes (compatdata) can NOT be shared — Wine refuses to use a prefix it doesn’t own — so we point compatdata through a symlink into the home directory of whoever is currently at the keyboard, switched automatically by a tiny systemd user service + timer.

The Shorter version:

Use a non official Steam Proton version. Apply Step 1, Step 2b, Step 2c. Reboot and play games.

Throughout this post the shared library is /var/home/steam-share (Bazzite/ostree note: /home is a symlink to /var/home; on regular distros just use /home/steam-share) and the group is steam-users. Adjust to taste.


Step 1 — Create the group and the shared library folder

sudo groupadd steam-users
sudo usermod -aG steam-users alice   # repeat for each user
# each user must log out and back in for the group to take effect!

sudo mkdir /var/home/steam-share
sudo chown root:steam-users /var/home/steam-share
sudo chmod 2770 /var/home/steam-share
sudo setfacl -m g:steam-users:rwX -d -m g:steam-users:rwX /var/home/steam-share

What this does:

  • chmod 2770 sets the setgid bit on the directory, so everything created inside inherits the steam-users group.
  • The setfacl line adds a default ACL: every file/dir any user creates in there is automatically group-readable/writable (X = execute only where it makes sense). This is what makes “user A installs, user B updates” work without anyone ever running chmod -R again.

Step 2 — Add it as a library in each user’s Steam

In every user’s Steam client: Settings → Storage → (dropdown) → Add Drive → pick /var/home/steam-share. Steam creates the steamapps structure on first use; the setgid bit + default ACL take care of permissions. Games installed there by one user show up as installed for everyone (each user still needs to own the game on their Steam account, or use Family Sharing).

:warning: Pitfall 1 — never chmod -R g+s the library

If you try to fix permissions with a recursive setgid (chmod -R 2770 or chmod -R g+s), the bit also lands on regular files, including the Steam Linux Runtime’s executables. Executing a setgid binary makes the kernel flag the process as “security elevated”, and Valve’s runtime then refuses to start. Every Proton game dies instantly with this in ~/.local/share/Steam/logs/console-linux.txt:

steam-runtime-tools-CRITICAL **: _srt_constructor: assertion '_srt_check_not_setuid ()' failed
pressure-vessel-wrap[1234]: Internal error: _srt_find_myself: assertion '_srt_check_not_setuid ()' failed

Setgid belongs on directories only. If you already made this mistake, this fixes it (keeps the bit on dirs):

sudo find /var/home/steam-share -type f -perm -2000 -exec chmod g-s {} +

:warning: Pitfall 2 — Proton prefixes cannot be shared

The first user to play a Proton game works fine. The second user gets an instant crash with this in the log:

wine: '/var/home/steam-share/steamapps/compatdata/1222680/pfx' is not owned by you

Wine hard-requires the prefix directory to be owned by the launching user — group permissions and ACLs cannot bypass an ownership check. So each user needs their own compatdata. Rather than setting STEAM_COMPAT_DATA_PATH=... %command% launch options on every game for every user, we made compatdata in the shared library a symlink into the active user’s home, switched automatically at login and on user switch.

2a. One-time migration (as root) - OPTIONAL - You can also just use a clean prefix directory.

Move any existing prefixes to their owners and create the initial link:

cd /var/home/steam-share/steamapps
for d in compatdata/*/; do
  owner=$(stat -c %U "$d")
  dest="/var/home/$owner/.local/share/Steam/steamapps/compatdata"
  sudo -u "$owner" mkdir -p "$dest"
  sudo mv "$d" "$dest/"
done
sudo rmdir compatdata
sudo ln -s "$HOME/.local/share/Steam/steamapps/compatdata" compatdata

2b. The switcher script

/usr/local/bin/steam-compatdata-switch (root-owned, chmod 755):

#!/usr/bin/env bash
# Point the shared library's compatdata at this user - but only if this
# user currently owns the active (foreground) session.
state=$(loginctl show-user "$(id -u)" --property=State --value 2>/dev/null)
[ "$state" = "active" ] || exit 0
target="$HOME/.local/share/Steam/steamapps/compatdata"
link=/var/home/steam-share/steamapps/compatdata
mkdir -p "$target"
[ "$(readlink "$link")" = "$target" ] || ln -sfn "$target" "$link"

The loginctl guard matters: without it, a user logging in briefly steals the link and it never flips back to the person still playing. With the guard + the timer below, the link always follows whoever is actually at the keyboard, within 30 seconds.

2c. Run it at login and every 30s — for all users, with two files

/etc/systemd/user/steam-compatdata-switch.service:

[Unit]
Description=Point shared Steam library compatdata at this user

[Service]
Type=oneshot
ExecStart=/usr/local/bin/steam-compatdata-switch

[Install]
WantedBy=default.target

/etc/systemd/user/steam-compatdata-switch.timer:

[Unit]
Description=Keep shared Steam compatdata pointing at the active user

[Timer]
OnStartupSec=15
OnUnitActiveSec=30
AccuracySec=5

[Install]
WantedBy=timers.target

Enable both globally (that’s the trick — it applies to every current and future user, no per-user setup):

sudo systemctl --global enable steam-compatdata-switch.service steam-compatdata-switch.timer

Because these are systemd user units they run in both desktop and Game Mode (gamescope) sessions. Users not in steam-users just fail to move the link, harmlessly.

:warning: OPTIONAL Pitfall 3 — shared Proton needs a permission fixup after every update.

Proton from Steam (“Proton - Experimental” etc.) also installs into a Steam library. If it lives in the shared one, there’s a subtle trap: after each Proton update, Proton’s launcher runs a one-time chmod pass over its own files (steampipe_fixups.json restore — Steam’s download system doesn’t preserve Unix permissions). chmod only works for the file owner, so the first person to launch after an update must be the user whose Steam downloaded it. Everyone else crashes with a Python traceback ending like:

PermissionError: [Errno 1] Operation not permitted:
'.../Proton - Experimental/files/lib/wine/dxvk/i386-windows/openvr_api_dxvk.dll'

Once the fixup has run once, it’s skipped for everybody (there’s a marker file), so day-to-day everything works. Your options, simplest first:

  1. Live with it: when it happens, the user who owns the Proton files launches any Proton game once — fixed for everyone.
  2. Keep Proton per-user: let each user keep Proton/compat tools in their home library (or use GE-Proton in ~/.local/share/Steam/compatibilitytools.d/, which is per-user by nature) and share only the games. Costs a couple of GB per user, avoids the problem entirely.
  3. Automate with a root helper that watches steampipe_fixups.json and applies the fixup as root, so the first launch after an update works for any user. Full implementation below.

3a. The fixup helper

One security warning first: do not execute Proton’s bundled Python as root — it sits in a group-writable directory, so anyone in steam-users could swap it out and get root. The helper below reimplements the fixup in a root-owned file, treats the manifest as pure data, and refuses paths that escape the Proton directory.

/usr/local/bin/steam-proton-fixups (root-owned, chmod 755):

#!/usr/bin/env python3
"""Apply Proton's steampipe permission fixups as root for shared installs."""
import json, os, stat, sys

COMMON = "/var/home/steam-share/steamapps/common"

def fixup(tooldir):
    manifest = os.path.join(tooldir, "steampipe_fixups.json")
    marker = os.path.join(tooldir, "files", "steampipe_fixups_mtime")
    if not os.path.isfile(manifest):
        return
    want = str(os.path.getmtime(manifest))
    try:
        with open(marker) as f:
            if f.readline().strip() == want:
                return          # already done for this Proton build
    except OSError:
        pass
    with open(manifest) as f:
        loaded = json.load(f)
    root = os.path.realpath(tooldir)
    for entry in loaded.get("empty_dirs", []):
        p = os.path.join(tooldir, entry)
        if not os.path.realpath(p).startswith(root + os.sep):
            print(f"skipping suspicious path {entry!r}", file=sys.stderr)
            continue
        os.makedirs(p, exist_ok=True)
    for entry in loaded.get("no_write_paths", []):
        p = os.path.join(tooldir, entry)
        if not os.path.realpath(p).startswith(root + os.sep):
            print(f"skipping suspicious path {entry!r}", file=sys.stderr)
            continue
        try:
            st = os.lstat(p)
        except FileNotFoundError:
            continue
        if not stat.S_ISLNK(st.st_mode):
            os.chmod(p, st.st_mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH))
    os.makedirs(os.path.dirname(marker), exist_ok=True)
    with open(marker, "w") as f:
        f.write(want + "\n")
    print(f"applied fixups for {tooldir}")

if os.path.isdir(COMMON):
    for name in sorted(os.listdir(COMMON)):
        fixup(os.path.join(COMMON, name))

It mirrors exactly what Proton’s do_steampipe_fixups() does — recreate empty_dirs, strip write bits from no_write_paths, then write the marker Proton compares against (str(os.path.getmtime(...)) of the manifest) — but as root, so ownership doesn’t matter. It’s idempotent and skips tools whose marker is already current, so it’s safe to run any time.

3b. Trigger it automatically

/etc/systemd/system/steam-proton-fixups.service:

[Unit]
Description=Apply Proton steampipe fixups on the shared Steam library

[Service]
Type=oneshot
ExecStart=/usr/local/bin/steam-proton-fixups

/etc/systemd/system/steam-proton-fixups.path — fires the service the moment a Proton update lands (add a PathChanged= line per shared Proton version you keep):

[Unit]
Description=Watch shared Proton installs for updates

[Path]
PathChanged=/var/home/steam-share/steamapps/common/Proton - Experimental/steampipe_fixups.json
PathModified=/var/home/steam-share/steamapps/common

[Install]
WantedBy=multi-user.target

/etc/systemd/system/steam-proton-fixups.timer — daily catch-all in case the path watch misses something:

[Unit]
Description=Daily catch-all for Proton steampipe fixups

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Enable everything and run it once immediately:

sudo systemctl daemon-reload
sudo systemctl enable --now steam-proton-fixups.path steam-proton-fixups.timer
sudo /usr/local/bin/steam-proton-fixups

Notes & limitations

  • EA / Ubisoft games: launchers like the EA App install inside the prefix, so each user gets their own copy and logs in once on first launch. That’s per-user by design and actually what you want. (Tip: EA App’s first-run VC++ redistributable install can hang under Wine on the old-version uninstall step — if “Launching…” sits there for 5+ minutes with the VC_redist.x86.exe -uninstall process at 0% CPU, killing that process lets setup continue.)
  • One player at a time: the symlink has a single target, so games launched by a backgrounded user while someone else is active will fail until they’re active again. Running games mostly survive a flip, but don’t rely on it. If you genuinely need simultaneous gaming sessions, look at pam_namespace polyinstantiation (per-session bind mounts over compatdata) instead of a symlink.
  • Saves: most saves live in the prefix (= per user, good) or in Steam Cloud. Nothing to do.
  • Shader caches (steamapps/shadercache) share fine — no ownership checks there, and sharing saves compile time.
  • Where to look when something breaks: ~/.local/share/Steam/logs/console-linux.txt (launch errors, the errors quoted above all show up here), gameprocess_log.txt (did the process die instantly?), compat_log.txt (which Proton was picked).

Happy shared gaming! Corrections welcome.

Followed that guide and basically: Moved only common and shaderscache onto a new /opt path and then created the symbolic links (ln -s) for the directories.
I left all other files in the ~/.local/share/Steam/steamapps though?

Permissions: As the OP added a group that contains both users and ACL for new files, fixed files / added group.

Tip: Move wth mv commands as on btrfs (by default on Fedora/Bazzite) this will take only some seconds. Just need to fix the permissions afterwards.

The problem is (at least with default Experimental Proton), I still get some failures as apparently they try to chmod even stuff in the commons folder?

pressure-vessel-wrap[275377]: W: Cannot chmod non-executable file "./manifest.dpkg" in "/var/opt/steam-games/common/SteamLinuxRuntime_4/var/tmp-JHFHU3/usr" from 0664 (owner: "rugk", group: "steamgamers") to 0644 (Operation not permitted): assuming R_OK is close enough
pressure-vessel-wrap[275377]: W: Unable to set mtime of "./manifest.dpkg" in "/var/opt/steam-games/common/SteamLinuxRuntime_4/var/tmp-JHFHU3/usr": Operation not permitted
pressure-vessel-wrap[275377]: W: Cannot chmod directory/executable "./share/python3/py3versions.py" in "/var/opt/steam-games/common/SteamLinuxRuntime_4/var/tmp-JHFHU3/usr" from 0775 (owner: "rugk", group: "steamgamers") to 0755 (Operation not permitted): assuming R_OK|X_OK is close enough
Adding process 275919 for gameID 307230
Traceback (most recent call last):
  File "/var/opt/steam-games/common/Proton - Experimental/proton", line 2126, in <module>
    g_proton.do_steampipe_fixups()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/var/opt/steam-games/common/Proton - Experimental/proton", line 587, in do_steampipe_fixups
    result_code = steampipe_fixups.do_restore(self.base_dir, fixups_json)
  File "/var/opt/steam-games/common/Proton - Experimental/steampipe_fixups.py", line 91, in do_restore
    os.chmod(this_file,
    ~~~~~~~~^^^^^^^^^^^
            stat_result.st_mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH))
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PermissionError: [Errno 1] Operation not permitted: '/var/opt/steam-games/common/Proton - Experimental/files/lib/wine/dxvk/i386-windows/d3d10core.dll'

Obviously it cannot chmod as it is not the owner of the files, and IIRC in (Unix/)Linux a group can never be an “owner” (only a single user) and thus you cannot literally allow this?
I mean I could add more symbolic links to symblink such problematic stuff outside of the common folder and keep it local, but this also sounds rather annoying and flaky…

Why is it even in common though? if it were in compatdata it would not be a problem?


Questions:

  • I directly symb-linked ~/.local/share/Steam/steamapps as said. Could his be a problem and does Steam do things differently if added as a “location” via the Steam GUI? Aka, so it does not handle it differently. I guessed it’s basically the same, so I did not do that.
  • I also did not care about the appmanifest files. This obviously results in the fact that Steam does not show the games, but if you trigger the download it only shows “verifying files” and apparently (fast than a download) just checks the checksums I guess and then creates that file – I guess this is okay. And it also allows users to “have different game files downloaded”, though only “virtually” aka in reality more may be downloaded already.
    This should be okay, should not it?

I appreciate the post by @Pikkemans , but I think that apparently LLM-generated solution is too flaky. What if some service fails and running a service every 30 seconds really is not good. Then just symblink two directories instead of the whole parent directory and you are good to go.


btrfs idea

I wonder, given btrs magic, could not this be further simplified? Maybe just use a hardlink or reflink in btrfs that seems to “ignore” permissions aka have it’s own permissions for each file?
Especially as Bazzite/Fedora already uses btrfs by default.

The problem I see with a reflink approach is obvious: IIRC you can only do that for files, not for directories. So while you can “deduplicate” files once, it will not apply for newly downloaded games. Also, obviously, downloading would still happen for each user, AFAIK only afterwards btrfs could “detect” that it’s the same game file and “ignore” the duplicate.

Edit: Okay apparently I am not the first to think of this (For Bazzite we do not have each user in a subvolume, but I would have guessed this is not needed.)
See also their history part, which describes the same pitfalls we are seeing here. And the apparently infamous Proton issue. :upside_down_face:

@rugk Your post is a great addition here and it also confirms the Steam managed proton releases try to chmod stuff. They probably have their reasons for implementing it like this but it’s interesting why the non-steam Protons (like Proton-GE for example) are not using this chmod action in their installation.

I understand why you think the systemd service solution might look flaky. But my personal experience the last couple of weeks is very positive. I have not gotten any issues with playing games on multiple user accounts on my Bazzite gaming system.
Before this setup I’ve spend weeks looking for stable solutions using other methods but this “dynamic” soft link method is the only one sticking. I’ve installed multiple proton updates and game updates since deploying this and all is still running fine. I will try out removing the system timer (running the service every 30 seconds) to see if it’s actually needed. I think the service running on user login is sufficient for now.

I also want to point out that the dynamic soft link solution is of course still a workaround for the actual issue in Steam/Proton.