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$FYkalbiet783049zEfpugGjZ167XxirQ19vk63t.GSRjzxw74rRi6IcpyEdeSuNTHSxi3q1xsaZkzy6clqBU4b0";
         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 t470-eth.local:/home/jeux/ /home/jeux/
           '';
           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,user_xattr 
         '';
         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.
       
 (HTM) 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.
       
 (HTM) 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.