Title: Creating a NixOS thin gaming client live USB | |
Author: Solène | |
Date: 20 May 2022 | |
Tags: nixos gaming | |
Description: I created a bootable USB media to play on my gaming | |
computer the games installed on my laptop | |
# Introduction | |
This article will cover a use case I suppose very personal, but I love | |
the way I solved it so let me share this story. | |
I'm a gamer, mostly on computer, but I have a big rig running Windows | |
because many games still don't work well with Linux, but I also play | |
video games on my Linux laptop. Unfortunately, my laptop only has an | |
intel integrated graphic card, so many games won't run well enough to | |
be played, so I'm using an external GPU for some games. But it's not | |
ideal, the eGPU is big (think of it as a big shoes box), doesn't have | |
mouse/keyboard/usb connectors, so I've put it into another room with a | |
screen at a height to play while standing up, controller in hands. | |
This doesn't solve everything, but I can play most games running on it | |
and allowing a controller. | |
But if I install a game on both the big rig and the laptop, I have to | |
manually sync the saves (I'm buying most of the games on GOG which | |
doesn't have a Linux client to sync saves), it's highly boring and | |
error-prone. | |
So, thanks to NixOS, I made a recipe to generate a USB live media to | |
play on the big rig, using the data from the laptop, so it's acting as | |
a thin client. The idea of a read only media to boot from is very | |
nice, because USB memory sticks are terrible if you try to install | |
Linux on them (I tried many times, it always ended with I/O errors | |
quickly) and there is exactly what you need, generated from a | |
declarative file. | |
What does it solve concretely? I can play some games on my laptop | |
anywhere on the small screen, I can also play with my eGPU on the | |
standing desk, but now I can also play all the installed games from the | |
big rig with mouse/keyboard/144hz screen. | |
# What's in the live image? | |
The generated ISO (USB capable) should come with a desktop environment | |
like Xfce, Nvidia drivers, Steam, Lutris, Minigalaxy and some other | |
programs I like to use, I keep the programs list minimal because I | |
could still use nix-shell to run a program later. | |
For the system configuration, I declare the user "gaming" with the same | |
uid as the user on my laptop, and use an NFS mount at boot time. | |
I'm not using Network Manager because I need the system to get an IP | |
before connecting to a user account. | |
# The code | |
I'll be using flakes for this, it makes pinning so much easier. | |
I have two files, "flake.nix" and "iso.nix" in the same directory. | |
flake.nix file: | |
```flake.nix | |
{ | |
inputs = { | |
nixpkgs.url = "nixpkgs/nixos-unstable"; | |
}; | |
outputs = { self, nixpkgs, ... }@inputs: | |
let | |
system = "x86_64-linux"; | |
pkgs = import nixpkgs { inherit system; config = { allowUnfree = true; };… | |
lib = nixpkgs.lib; | |
in | |
{ | |
nixosConfigurations.isoimage = nixpkgs.lib.nixosSystem { | |
system = "x86_64-linux"; | |
modules = [ | |
./iso.nix | |
"${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-base.nix" | |
]; | |
}; | |
}; | |
} | |
``` | |
And iso.nix file: | |
```iso nix code | |
{ config, pkgs, ... }: | |
{ | |
# compress 6x faster than default | |
# but iso is 15% bigger | |
# tradeoff acceptable because we don't want to distribute | |
# default is xz which is very slow | |
isoImage.squashfsCompression = "zstd -Xcompression-level 6"; | |
# my azerty keyboard | |
i18n.defaultLocale = "fr_FR.UTF-8"; | |
services.xserver.layout = "fr"; | |
console = { | |
keyMap = "fr"; | |
}; | |
# xanmod kernel for better performance | |
# see https://xanmod.org/ | |
boot.kernelPackages = pkgs.linuxPackages_xanmod; | |
# prevent GPU to stay at 100% performance | |
hardware.nvidia.powerManagement.enable = true; | |
# sound support | |
hardware.pulseaudio.enable = true; | |
# getting IP from dhcp | |
# no network manager | |
networking.dhcpcd.enable = true; | |
networking.hostName = "biggy"; # Define your hostname. | |
networking.wireless.enable = false; | |
# many programs I use are under a non-free licence | |
nixpkgs.config.allowUnfree = true; | |
# enable steam | |
programs.steam.enable = true; | |
# enable ACPI | |
services.acpid.enable = true; | |
# thermal CPU management | |
services.thermald.enable = true; | |
# enable XFCE, nvidia driver and autologin | |
services.xserver.desktopManager.xfce.enable = true; | |
services.xserver.displayManager.lightdm.autoLogin.timeout = 10; | |
services.xserver.displayManager.lightdm.enable = true; | |
services.xserver.enable = true; | |
services.xserver.libinput.enable = true; | |
services.xserver.videoDrivers = [ "nvidia" ]; | |
services.xserver.xkbOptions = "eurosign:e"; | |
time.timeZone = "Europe/Paris"; | |
# declare the gaming user and its fixed password | |
users.mutableUsers = false; | |
users.users.gaming.initialHashedPassword = "$6$bVayIA6aEVMCIGaX$FYkalbiet7830… | |
users.users.gaming = { | |
isNormalUser = true; | |
shell = pkgs.fish; | |
uid = 1001; | |
extraGroups = [ "networkmanager" "video" ]; | |
}; | |
services.xserver.displayManager.autoLogin = { | |
enable = true; | |
user = "gaming"; | |
}; | |
# mount the NFS before login | |
systemd.services.mount-gaming = { | |
path = with pkgs; [ nfs-utils ]; | |
serviceConfig.Type = "oneshot"; | |
script = '' | |
mount.nfs -o fsc,nfsvers=4.2,wsize=1048576,rsize=1048576,async,noatime t4… | |
''; | |
before = [ "display-manager.service" ]; | |
wantedBy = [ "display-manager.service" ]; | |
after = [ "network-online.target" ]; | |
}; | |
# useful packages | |
environment.systemPackages = with pkgs; [ | |
bwm_ng | |
chiaki | |
dunst # for notify-send required in Dead Cells | |
file | |
fzf | |
kakoune | |
libstrangle | |
lutris | |
mangohud | |
minigalaxy | |
ncdu | |
nfs-utils | |
steam | |
steam-run | |
tmux | |
unzip | |
vlc | |
xorg.libXcursor | |
zip | |
]; | |
} | |
``` | |
Then I can update the sources using "nix flake lock --update-input | |
nixpkgs", that will tell you the date of the nixpkgs repository image | |
you are using, and you can compare the dates for updating. I recommend | |
using a program like git to keep track of your files, if you see a | |
failure with a more recent nixpkgs after the lock update, you can have | |
fun pinpointing the issue and reporting it, or restoring the lock to | |
the previous version and be able to continue building ISOs. | |
You can build the iso with the command "nix build | |
.#nixosConfigurations.isoimage.config.system.build.isoImage", this will | |
create a symlink "result" in the directory, containing the ISO that you | |
can burn on a disk or copy to a memory stick using dd. | |
# Server side | |
Of course, because I'm using NFS to share the data, I need to configure | |
my laptop to serves the files over NFS, this is easy to achieve, just | |
add the following code to your "configuration.nix" file and rebuild the | |
system: | |
```configuration.nix | |
services.nfs.server.enable = true; | |
services.nfs.server.exports = '' | |
/home/gaming 10.42.42.141(rw,nohide,insecure,no_subtree_check) | |
''; | |
``` | |
If like me you are using the firewall, I'd recommend opening the NFS | |
4.2 port (TCP/2049) on the Ethernet interface only: | |
```configuration.nix | |
networking.firewall.enable = true; | |
networking.firewall.allowedTCPPorts = [ ]; | |
networking.firewall.allowedUDPPorts = [ ]; | |
networking.firewall.interfaces.enp0s31f6.allowedTCPPorts = [ 2049 ]; | |
``` | |
In this case, you can see my NFS client is 10.42.42.141, and previously | |
the NFS server was referred to as laptop-ethernet.local which I declare | |
in my LAN unbound DNS server. | |
You could make a specialisation for the NFS server part, so it would | |
only be enabled when you choose this option at boot. | |
# NFS performance improvement | |
If you have a few GB of spare memory on the gaming computer, you can | |
enable cachefilesd, a service that will cache some NFS accesses to make | |
the experience even smoother. You need memory because the cache will | |
have to be stored in the tmpfs and it needs a few gigabytes to be | |
useful. | |
If you want to enable it, just add the code to the iso.nix file, this | |
will create a 10 MB * 300 cache disk. As tmpfs lacks user_xattr mount | |
option, we need to create a raw disk on the tmpfs root partition and | |
format it with ext4, then mount on the fscache directory used by | |
cachefilesd. | |
```nix code | |
services.cachefilesd.enable = true; | |
services.cachefilesd.extraConfig = '' | |
brun 6% | |
bcull 3% | |
bstop 1% | |
frun 6% | |
fcull 3% | |
fstop 1% | |
''; | |
# hints from http://www.indimon.co.uk/2016/cachefilesd-on-tmpfs/ | |
systemd.services.tmpfs-cache = { | |
path = with pkgs; [ e2fsprogs busybox ]; | |
serviceConfig.Type = "oneshot"; | |
script = '' | |
if [ ! -f /disk0 ]; then | |
dd if=/dev/zero of=/disk0 bs=10M count=600 | |
echo 'y' | mkfs.ext4 /disk0 | |
fi | |
mkdir -p /var/cache/fscache | |
mount | grep fscache || mount /disk0 /var/cache/fscache -t ext4 -o loop,use… | |
''; | |
before = [ "cachefilesd.service" ]; | |
wantedBy = [ "cachefilesd.service" ]; | |
}; | |
``` | |
# Security consideration | |
Opening an NFS server on the network must be done only in a safe LAN, | |
however I don't consider my gaming account to contain any important | |
secret, but it would be bad if someone on the LAN mount it and delete | |
all the files. | |
However, there are two NFS alternatives that could be used: | |
* using sshfs using an SSH key that you transport on another media, but | |
it's tedious for a local LAN, I've been surprised to see sshfs | |
performance were nearly as good as NFS! | |
* using sshfs using a password, you could only open ssh to the LAN, | |
which would make security acceptable in my opinion | |
* using WireGuard to establish a VPN between the client and the server | |
and use NFS on top of it, but the secret of the tunnel would be in the | |
USB memory stick so better not have it stolen | |
# Size optimization | |
The generated ISO can be reduced in size by removing some packages. | |
## Gnome | |
for example Gnome comes with orca which will bring many dependencies | |
for text-to-speech. You can easily exclude many Gnome packages. | |
``` | |
environment.gnome.excludePackages = with pkgs.gnome; [ | |
pkgs.orca | |
epiphany | |
yelp | |
totem | |
gnome-weather | |
gnome-calendar | |
gnome-contacts | |
gnome-logs | |
gnome-maps | |
gnome-music | |
pkgs.gnome-photos | |
]; | |
``` | |
## Wine | |
I found that Wine came with the Windows compiler as a dependency, but | |
yet it doesn't seem useful for running games in Lutris. | |
NixOS discourse: Wine installing mingw32 compiler? | |
It's possible to rebuild Wine used by Lutris without support for the | |
mingw compiler, replace the lutris line in the "systemPackages" list | |
with the following code: | |
``` | |
(lutris-free.override { | |
lutris-unwrapped = lutris-unwrapped.override { | |
wine = wineWowPackages.staging.override { | |
mingwSupport = false; | |
}; | |
}; | |
}) | |
``` | |
Note that I'm using lutris-free which doesn't support Steam because it | |
makes it a bit lighter and I don't need to manage my Steam games with | |
Lutris. | |
# Possible improvements | |
It could be possible to try getting a package from the nix-store on the | |
NFS server before trying cache.nixos.org which would improve bandwidth | |
usage, it's easy to achieve but yet I need to try it in this context. | |
# Issue | |
I found Steam games running with Proton are slow to start. I made a bug | |
report on the Steam Linux client github. | |
Github: Proton games takes around 5 minutes to start from a network share | |
This can be solved partially by mounting | |
~/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var as | |
tmpfs, it will uses less than 650MB. | |
# Conclusion | |
I really love this setup, I can backup my games and saves from the | |
laptop, play on the laptop, but now I can extend all this with a bigger | |
and more comfortable setup. The USB live media doesn't take long to be | |
copied to a USB memory stick, so in case one is defective, I can just | |
recopy the image. The live media can be booted all in memory then be | |
unplugged, this gives a crazy fast responsive desktop and can't be | |
altered. | |
My previous attempts at installing Linux on an USB memory stick all | |
gave bad results, it was extremely slow, i/o errors were common enough | |
that the system became unusable after a few hours. I could add a small | |
partition to one disk of the big rig or add a new disk, but this will | |
increase the maintenance of a system that doesn't do much. |