Back Original

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.