My NixOS dev machine
I have a home server that I built primarily for “cloud” gaming (read more about
that here). I’m running Proxmox so I can install other services, one of which
is my notes machine. After many years of experimentation, I’ve landed on a
system that I like: I ssh notes@notes which drops me directly into nvim -c VimwikiMakeDiaryNote and disconnects on close. It’s a very lightweight version
of an SSH app, which I’m starting to realize I like quite a bit.
So I decided to setup a development machine and access it like my notes
server: ssh project@dev and drop into a tmux session with all dependencies
installed. That’s the premise: one user per project in isolated environments.
I heard NixOS is pretty good at isolated environments and dependency
management, so I decided to try it out! So far it’s been fun defining a system
declaratively:
environment.systemPackages = with pkgs; [
claude-code
curl
file
ghostty
htop
jq
putScript
ripgrep
tree
wget
yankScript
];
These packages are available system-wide. Oh, and yankScript and putScript
are custom shell scripts defined as:
yankScript = pkgs.writeShellScriptBin "yank" ''
# If args are passed, copy those, otherwise, read stdin.
if [ $# -gt 0 ]; then
content="$*"
else
# Use process substitution to prevent command substitution from
# stripping trailing newlines.
IFS= read -rd "" content < <(cat)
fi
printf '%s' "$content" > /tmp/clipboard
if [ -n "$TMUX" ]; then
tmux set-buffer -w "$content"
else
printf "\033]52;c;%s\007" $(base64 -w0 <<< "$content")
fi
'';
putScript = pkgs.writeShellScriptBin "put" ''
cat /tmp/clipboard
'';
I’ll talk more about these in another post, but this framework is pretty neat.
The one oddity is that I do find myself writing a lot of large text blocks like above.
This seems to be the pattern for tmux, neovim and zsh configurations.
But the thing I really like about this is how I’m able to define users (which are projects)!
[
{ name = "blog"; origin = "git@github.com:miccah/miccah.github.io"; }
{ name = "repost"; origin = "git@github.com:miccah/repost"; }
{ name = "ipf"; origin = "git@github.com:miccah/immich-photo-frame"; }
{ name = "pg"; origin = "git@github.com:miccah/just-use-postgres"; }
{ name = "scratch"; }
]
This is the list of projects! It automatically sets up the user, installs
authorized SSH keys, initializes git with the remote (whether it exists or
not), and populates the project with a template flake. And this isn’t some
magic that is happening, it is an interface that I created for a system that I
want.
So configuring the system declaratively for all users (projects) has been really fun, but one of the goals of this project was dependency management and automatically having isolated tools available. That’s where project flakes come in:
{
description = "miccah.io";
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; };
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
hugo
];
};
};
}
This is a flake for my blog, which is a static site generated by hugo. Note
the hugo package is installed. Whenever I’m in blog@dev, hugo is
automatically there.
And that’s pretty much it! So far this system has been fun and flexible enough to allow remote development of web servers, desktop applications, and even embedded projects. I’m really happy with the ergonomics (which I’ll talk more about in another post), but one thing I would really love is to be able to easily pair program with it. I think that will be an entire project of its own, though.