When I first started using Linux in 2006 I remember dreaming of a Linux Console. The idea maybe wasn't so far fetched at the time, the PlayStation 3 had just been released with OtherOS support which allowed users to install Linux (or BSD). Still, it seemed that a Linux-first console would only ever be a dream. Now in 2022, Valve's Steam Deck is a hackable Linux-first portable console.
Today, we'll be putting Nix on it, because what's Linux without Nix?
Just wanna try it? Jump to the fun part.
Want NixOS instead? A different, spicier kind of fun can be found here.
The Steam Deck is a portable computer that has a Nintendo Switch-like form factor and touts an AMD x86_64 processor. It has WiFi, Bluetooth, and a USB-C port which you can plug a hub into, allowing the attachment of HDMI, mice, keyboards, power, or ethernet cables.
It runs a flavour of Arch Linux called SteamOS, and guides users to use Flatpak and Flathub. This is a fantastic solution and provides users access to a wide variety of software, but as a developer I tend to want more exotic stuff that exists in
In case you'd not seen one yet, here's a picture of mine:
Installing Nix on the Steam Deck has a few special steps. Let's review how a Nix install process looks, then how the Deck works, and finally we can explore a working approach to install Nix.
How a Nix install works
A normal Nix install process on Linux works roughly like this:
- Create a folder called
- Unpack the Nix distribution tarball into
- Create some Nix daemon users and a group (affecting
systemctl linkon some systemd units from
- Sprinkle some magic in the detected shell profiles (in
/etc) to ensure
On Mac, where creating a
/nix is forbidden (by the creators, Apple), we can modify
/etc/synthetic.conf to create a stub which we can mount an APFS volume to. The installation otherwise proceeds as normal.
On the Steam Deck, creating
/nix also requires special steps. Unfortunately, there is no feature similar to
Why does the Deck need these special steps? Let's take a look at the Steam Deck itself and figure out why we can't just run the familiar Nix installer.
The Deck & SteamOS
The Steam Deck ships with an Arch Linux based distribution called SteamOS -- a special image of it to be even more precise. Normally Arch Linux is a perfectly fine target for Nix, but there are a couple particularities around this distribution that impact how we can install Nix.
The Deck uses an A/B boot system (like some Android phones), which means it has two parallel installations, booting into one and updating the other. This means, if it ever fails after an update it can safely roll back to a known good state.
NixOS user? Sound familiar? It's like the generation selector in your bootloader, but instead of pointing your boot to different Nix store paths, it points to entirely different partitions!
(deck@steamdeck ~)$ sudo gdisk /dev/nvme0n1 -l Number Start (sector) End (sector) Size Code Name 1 2048 133119 64.0 MiB EF00 esp 2 133120 198655 32.0 MiB 0700 efi-A 3 198656 264191 32.0 MiB 0700 efi-B 4 264192 10749951 5.0 GiB 8304 rootfs-A 5 10749952 21235711 5.0 GiB 8304 rootfs-B 6 21235712 21759999 256.0 MiB 8310 var-A 7 21760000 22284287 256.0 MiB 8310 var-B 8 22284288 1000215175 466.3 GiB 8302 home
See how there is
B copies of most partitions?
This looks a heck of a lot different than my development machine:
Number Start (sector) End (sector) Size Code Name 1 2048 2099199 1024.0 MiB EF00 efi 2 2099200 3907029134 1.8 TiB 8309 encrypt
Checking for encryption with
blkid | grep crypto_LUKS showed all partitions were unencrypted, this makes sense since the Deck never asks for a password, even for
sudo, until you set one. It's a bit unfortunate Valve did not opt to protect their user's data in the event this portable device was stolen, but it's room to improve.
This A/B boot system means even if
rootfs partitions get modified, those changes may get wiped out at any time. The system may update or choose to boot into the other 'letter' for some other reason. We want something that is update-proof and will survive a change of 'letter'.
One partition that persists across reboots and has enough space to contain a thick, chunky Nix store is the
home partition. Our Nix install can keep persistent data there.
mount output is a bit misleading. While the
/ mount says it is
rw, it is normally not.
() | ) ) ) ) ) ) ) ) ) ) ) ) ()
This isn't a scary vendor lockdown security feature or anything, it's mostly to prevent the user from being surprised when the A/B boot happens. SteamOS comes with a
steamos-readonly executable we can use to toggle this read-only feature at any time, this can allow us to make small changes to the root filesystem as long as we don't expect them to persist across boots.
Because of this, if we wanted, we could create a
/nix path on the
rootfs each boot by making the root momentarily writable.
Not all of the device is read-only though! We can write to places like
/etc/, but not to
() () ()) ()) ())
Recalling the rough steps from the install process, this isn't a problem! So long as we work out the machinery to ensure
/nix is available, the Steam Deck looks otherwise like a normal system to Nix.
Enabling an Install
As we discovered, creating the
/nix directory in a safe way that persists will be our primary challenge.
Since it wouldn't be a great idea to store the Nix Store on the
rootfs partitions, we must decide somewhere else. The most immediately obvious answer is
/home/nix, since that is a large, persistent location.
With an existing
/home/nix, we can use a bind mount to mount that to
/nix. First, a
/nix path needs be created somehow!
/etc writable, we can drop systemd units into
/etc/systemd/system that will set up
/nix for us.
We'll create a
nix-directory.service unit which creates the
/nix path, and a
nix.mount unit which depends on that.
Sadly, that's not quite enough to enable a full install though. Since the Nix install process involves
systemctl link $UNIT, some of the systemd units are not available during systemd's startup. Therefore we must reload the systemd daemon itself after the
nix.mount unit is started. In order to do that, we follow the same method as Flatcar Linux does here.
Let's cover what these units look like then test them out with the Nix installer! If you're feeling brave I invite you to help us test an experimental Nix installer we've been working on which has a special codepath just for the Steam Deck. Otherwise, follow along below to try the traditional install script.
But first, just in case:
- Not sure how to get to 'Desktop mode'? Hit the Steam button, go to 'Power', go to 'Switch to Desktop'
- Not sure how to get a terminal? In 'Desktop Mode' hit the logo in the bottom left corner, in the search bar type "Terminal", select 'Konsole'
- Not sure how to edit files? You can use
vimif you are familiar, otherwise try
nanofrom the terminal.
Putting it all together
Want to follow along without a Deck? Learn how to set up a Deck VM with this article.
There are only four Steam Deck specific steps, three are to create the following systemd units. The final one is to enable one of those units.
Create the following systemd units at the noted paths, I suggest using a keyboard plugged into the Deck if you can, or enable SSH via
sudo systemctl start sshd, reviewing the IP address via
ip a, and setting a password. If those options are unavailable, hit the Steam and X buttons to summon the keyboard.
[Unit] Create a `/nix` directory to be used for bind mounting nix-daemon.service nix.mount no grub-recordfail.service steamos-finish-oobe-migration.service [Service] oneshot steamos-readonly disable mkdir -vp /nix chmod -v 0755 /nix chown -v root /nix chgrp -v root /nix steamos-readonly enable steamos-readonly disable rmdir /nix steamos-readonly enable true
The above unit is the first in our chain of units, it checks if a
/nix folder exists, and if necessary, calls
steamos-readonly disable, creates
/nix, then calls
steamos-readonly enable again. It also attempts to do some cleanup as it stops, but that part is unnecessary.
[Unit] Mount `/home/nix` on `/nix` nix-daemon.service nix-directory.service nix-directory.service nix-directory.service /nix no nix-daemon.service nix-daemon.socket [Mount] /home/nix /nix none 0755 bind
This mount unit performs a bind mount from
/nix. It'll create
/home/nix for us, but sadly it cannot create
/nix, relying on the
nix-directory.service before it.
[Unit] Ensure Nix related units which are symlinked resolve nix.mount nix-directory.service nix.mount no [Service] oneshot yes /usr/bin/systemctl daemon-reload /usr/bin/systemctl restart --no-block nix-daemon.socket [Install] sysinit.target
This final unit in the chain restarts the systemd daemon, allowing it to properly resolve any previously broken symlinks during the boot, before starting or enabling them if necessary.
Tailscale user? A similar strategy can be used after performing
systemd-sysext mergeif you happen to also use Tailscale on your Steam Deck to make sure it starts at boot.
After creating the units, we need to enable (and start) the last, causing the ones it requires to also start:
Now we can just run the Nix installer like normal:
Follow the prompts, call
exec $SHELL (or open a new shell, or reboot) and Nix should work on command line!
() () ) ()
Feel free to reboot a few times, or even update your Steam Deck. As far as I've experimented, it should keep working!
If you cause a instant, hard, full power loss (such as Ctrl+C'ing the VM) before it can properly
fsync(), you may see an error like
error: expected string 'Derive(['. To resolve this error, run
nix store gc. You can avoid this by running
sync before killing the device.
An invitation to experiment
Part of the reason we wanted to explore Nix on the Steam Deck is that we're currently experimenting with a new Nix installer, and we were curious what we could learn from adding support for a specific device which had special requirements, such as the Steam Deck.
If you feel like experimenting (and don't mind things breaking) feel encouraged to try out our prototype! Don't worry, if you don't like it, it includes an uninstaller so you can roll back and do your install with the traditional scripts.
You can run it like so:
If you don't feel like being experimental, this is what it looks like:
() | ) ) ) ) ) ) ) ) ) () ()
Hate it? Uninstall it:
(deck@steamdeck ~)$ /nix/nix-installer uninstall
Our prototype has the working name of
nix-installer. It supports different installation 'planners' (such as the
steam-deck), can be used as a Rust library, has fine grained logging, and can uninstall a Nix it installed.
It has no runtime dependencies (though it will try to
sudo itself if you forget) or build time dependencies (other than Rust/C compilers) and should build trivially inside or outside Nix for x86_64 and aarch64, Linux (
musl based) and Mac.
We are currently distributing fully reproducible and hermetic
nix based experimental builds for all supported platforms. The installer is Open Source (LGPL) and written in entirely in Rust. (Nix is still not in Rust -- sorry!)
You are welcome to explore the code here. Don't worry, we're very excited to talk about it at length in a future article. Stay tuned for more!
We've been working with other installer working group contributors like (alphabetical) Cole, Michael, Solène, Théophane, Travis, and others to build
nix-installerand better understand what a next-generation Nix installer would look like, thank you so much for all your help, hard work, and advice.
We explored how the Steam Deck takes certain measures to protect users from accidently losing changes when the system updates and swaps due to its A/B booting, we also explored how we can use persistent systemd units to create a
/nix path on the Steam Deck which bind mounts to a persistent
/home/nix directory. In order to ensure that the units linked from the
/nix path are loaded, we also learnt we can have a unit which reloads the systemd daemon, and how this resolves the issue.
Using these techniques, we successfully installed Nix on the Steam Deck using both the traditional installer, as well as a prototype that we've been working on.