My KDE desktop carried over from the migration untouched. The system underneath it changed completely.

I ran Debian for years. Debian 13 with KDE Plasma was my daily driver — stable, dependable, unremarkable in the best way. A few weeks ago, I wiped the root partition and replaced it with NixOS. The /home partition came along intact. KDE Plasma booted up looking exactly as it had before: same panel, same theme, same shortcuts.

That was the most unexpected part of the whole process. Everything I saw was identical. Everything managing it was different.

This post covers that migration through an unconventional angle: I didn't hand-write my first NixOS flake configuration myself. An AI CLI assistant did — OpenCode running the MiMo-V2.5 model — and I learned NixOS by reviewing, correcting, and iterating on its output.


Why I Switched

Three reasons, in order of how much they drove the decision.

1. Declarative system configuration. This is NixOS's headline feature and usually the first thing anyone hears about it. The entire operating system — packages, services, users, fonts, desktop — is defined in one set of text files. Version-controlled. Reproducible. No more tracking down which config file was edited months ago to make something work.

2. Reproducibility. On Debian, my system was an accumulation of years of apt install commands, half-remembered tweaks, and config files I was hesitant to touch because I no longer knew what depended on them. A reinstall meant losing all of it. I wanted a system I could rebuild from a Git repository onto any machine.

3. Curiosity about the Nix language. I'd done infrastructure-as-code work with Ansible and Docker before. Nix is a genuine functional programming language, and the idea of describing an entire OS as an expression — with a functional language's guarantees around build purity — was appealing.

I'm not new to Linux or to declarative tooling, but NixOS still humbled me. That's the honest framing: the concepts were familiar in spirit, unfamiliar in implementation.


The Setup: Fresh /, Preserved /home

The installation was a straightforward swap: a fresh NixOS install onto /, with /home kept as a separate, untouched partition.

This decision shaped everything that followed, for a few reasons:

  • KDE Plasma configuration lives in ~/.config, ~/.local/share, and similar user-space paths — not under /etc. Reinstalling / while keeping /home meant all of my Plasma settings carried over automatically. No export step, no backup choreography.
  • The applications I use daily (kate, wezterm, obsidian, bitwarden-desktop) had their data and settings in /home, ready to go.
  • System-level components — packages, services, networking — were the part I rebuilt from scratch in NixOS.

The migration split cleanly along that line: user state survived intact, while system state was rebuilt declaratively.


Writing My First Flake, With a Opencode

This is where my approach diverged from most migration write-ups.

Rather than studying the NixOS manual for a week and hand-typing my first configuration, I gave the task to OpenCode and let MiMo-V2.5 generate my flake structure. I'd run a nixos-rebuild switch, hit an error, paste it back, and watch the configuration get corrected. I learned NixOS the way you learn an unfamiliar codebase — by reading and fixing generated code against real feedback, rather than starting from the specification.

To be clear, this wasn't automated or hands-off. The AI surfaced real structure and caught genuine syntax errors, but I still had to understand every line before accepting it. The value was velocity and a tutor, not delegation. Every decision that made it into the final config passed through my own judgment.

Here's the flake root my setup arrived at:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, home-manager }:
    let
      system = "x86_64-linux";
    in {
      nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
        inherit system;
        modules = [
          ./hosts/nixos/default.nix
          ./modules/battery-alert.nix
          ./modules/cloudflare-warp.nix
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.backupFileExtension = "hm-bak";
            home-manager.users.nixos = import ./home/default.nix;
          }
        ];
      };
    };
}

Two inputs: nixpkgs pinned to nixos-unstable, and home-manager following that same nixpkgs. That follows declaration matters — it prevents the system and home-manager from resolving different nixpkgs snapshots and conflicting on package versions.

The resulting structure kept a clean separation of concerns:

/etc/nixos/
├── flake.nix                     # Entry point
├── hosts/nixos/                  # Host-specific settings
├── home/                         # home-manager, per-user dotfiles
├── modules/                      # System-level NixOS modules
└── pkgs/                         # Custom packages

System-level configuration goes in modules/. User-level configuration goes in home/. That boundary turned out to be the single most important mental model for everything that followed.


Packages: Two Worlds, One Clear Rule

The first real "aha" moment was understanding how package ownership splits on NixOS. On Debian, everything went through apt install — a single bucket. On NixOS, there are two:

  • System packagesenvironment.systemPackages in a NixOS module
  • User packageshome.packages in home-manager

The rule I settled on: system packages are things every user of the machine needs; user packages are my personal tools. It's a clean distinction, and one that holds up on multi-user setups.

System packages, in modules/packages.nix:

{ config, pkgs, ... }:

{
  virtualisation.docker.enable = true;

  nixpkgs.config.allowUnfree = true;
  nixpkgs.overlays = [
    (self: super: {
      settemp = super.callPackage ../pkgs/settemp {};
      battery-alert = super.callPackage ../pkgs/battery-alert {};
    })
  ];

  environment.systemPackages = with pkgs; [
    vim
    wget
    curl
    git
    bmon
    unzip
    settemp
    battery-alert
    libnotify
    nordic
    onlyoffice-desktopeditors
  ];

  fonts.packages = with pkgs; [
    nerd-fonts.jetbrains-mono
    corefonts
    vista-fonts
  ];
}

User packages, in home/packages.nix:

{ pkgs, ... }:

{
  home.packages = with pkgs; [
    kdePackages.kate
    btop
    htop
    bitwarden-desktop
    tree
    tmux
    wezterm
    nodejs
    evolution
    obsidian
  ];
}

Notice what's largely absent here: KDE Plasma itself. That's deliberate, and it's the whole point of this post.

One gotcha worth flagging: I avoided listing docker and docker-compose under home.packages once virtualisation.docker.enable = true; is set at the system level. The system module already provides the daemon and CLI for every user — duplicating it in home-manager is redundant and simply adds noise to your closure.


KDE Plasma: Same Desktop, Different Plumbing

Here's the point I keep returning to. My KDE desktop didn't migrate. It was already there.

Because Plasma stores its configuration in user space, and /home was preserved, the desktop booted up looking identical: panel layout, window decorations, Breeze theme, and fonts all present with zero export ceremony.

So where did the migration actually show up?

In how that configuration is managed. On Debian, Plasma settings were files I edited through the system settings GUI, silently writing to ~/.config with no history and no explanation. On NixOS, the parts of my Plasma experience that matter to me are now declared:

  • Dotfile management lives in home-manager, with backupFileExtension = "hm-bak" so any pre-existing manual configuration gets backed up rather than overwritten on first deploy.
  • The Konsole profile — my terminal appearance — is managed at ~/.local/share/konsole/Default.profile via home-manager, instead of existing as an obscure hand-edited file.
  • Native KDE applications like kate install via kdePackages.kate, a package set versioned in lockstep with the Plasma release.

The underlying shift is this: from a desktop state that is whatever happened to accumulate, to a desktop state that is declared, reproducible, and rebuildable. Same pixels, different provenance.


The Learning Curve: Module System + Mindset Shift

My two hardest challenges were both conceptual rather than mechanical.

The module system. Where do you even look for a given option? services.sddm.enable. environment.systemPackages. fonts.packages. home-manager.users.nixos. Each is a path into a large, structured tree of options. The difficulty isn't writing Nix syntax — it's knowing which option exists and under which namespace. This is where the AI assistant proved most useful: I'd describe the intent ("set up SDDM login"), and it would produce the right option path. I'd then verify against the official documentation, learning the shape of the option tree as a side effect.

The shift from imperative to declarative thinking. This deserves more credit than it typically gets. Years of apt install foo had trained me to think of adding software as an action. NixOS reframes it as a declaration — edit the configuration, rebuild, and the system state converges to match it. That sounds trivial to state in a sentence, but it required real repatterning in practice. The AI helped here too, but only as a scaffold; the mental shift itself was mine to make, one rebuild cycle at a time.

The rebuild loop became my feedback cycle:

sudo nixos-rebuild switch --flake /etc/nixos/#nixos

Edit configuration → rebuild → read the error → fix → repeat. nix flake check caught structural problems before I even triggered a rebuild. Watching the AI work through evaluation errors taught me more about how Nix resolves the dependency graph than any tutorial had.


What Surprised Me

A candid assessment of pros and cons after a few weeks of daily use.

The wins:

  • Rollbacks are reliably uneventful. Something broke; I switched to a previous generation and it was resolved. On Debian, that same situation meant a frantic apt archaeology session. Here, it's a boot menu entry.
  • System state is a Git repository. /etc/nixos is version-controlled. My entire OS is diffable, revertible, and reviewable.
  • Reproducibility holds up in practice. Choose the hardware, check out the configuration, rebuild, done.

The frictions:

  • Evaluation errors are the main obstacle. The type errors and namespace mistakes surfaced during nixos-rebuild represent the steepest part of the learning curve — readable once you're familiar with them, difficult at first.
  • Option hunting takes time. More than once I wanted to set a single value and spent twenty minutes locating the right option path. Options rarely live where instinct suggests.
  • Unfree and non-packaged software require extra work. allowUnfree = true covers most cases, but anything outside nixpkgs means writing or sourcing a derivation. My settemp utility and battery-alert, both tools I use daily, needed custom derivations under pkgs/, wired in via overlays. That's real flexibility, but also a responsibility Debian never asked of me.

Should You Do It?

Here's my honest assessment.

NixOS is worth the learning curve if you already think in declarative terms. If you've worked with Ansible, Terraform, Helm, or even Dockerfiles and found yourself wishing your entire OS could work that way, NixOS is the natural endpoint of that instinct. The concepts transfer. The syntax doesn't, but the mindset does — and the mindset is the harder part.

The AI-assisted path is genuinely faster. Not because it produces perfect configuration on the first pass, but because it compresses weeks of "where is this option" into an afternoon of "reasonable candidate, now verify it." It functions as a tutor that's available whenever the rebuild fails, and reading and correcting generated Nix against real errors turned out to be an effective way to internalize the module system.

Practical migration tips:

  1. Keep /home separate. User state carries your desktop; let NixOS take over the system layer.
  2. Use home-manager as a NixOS module (useGlobalPkgs, plus a backup extension so the first deploy doesn't overwrite existing dotfiles).
  3. Split system and user packages from day one. Avoid dumping everything into environment.systemPackages.
  4. Pin home-manager to follow your nixpkgs input. Version skew between them is a classic source of errors.
  5. Let the rebuild loop be your teacher: run nix flake check first, then sudo nixos-rebuild switch.

My KDE desktop looks exactly the same as it did on Debian 13. That's the point, and it's also the trick: the desktop I see is now a declaration I can rebuild anywhere, rather than an accumulation I'm hesitant to touch. That difference was worth the whole migration.


Where to Go Next

If you're considering NixOS and hesitant about the learning curve, try it with a capable AI assistant alongside you and a preserved /home behind you. The path may be shorter than expected.