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