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