From Migration Anxiety to One-Command Setups: Why I Bet My Dev Environment on Nix
As software engineers, we don’t just write code. We curate ecosystems.
From Migration Anxiety to One-Command Setups: Why I Bet My Dev Environment on Nix

nix package manager
As software engineers, we don’t just write code. We curate ecosystems.
My machine runs hundreds of tools: language runtimes, CLI utilities, shell plugins, container runtimes, linters, formatters, database clients, and a sprawling ~/.config directory. Over years of projects, upgrades, and trial-and-error, my environment became a fragile house of cards. It worked because I knew exactly which commands I ran and which PATH entries I forgot to document.
Then came the hardware shift: Intel Mac → Apple Silicon Mac.
macOS offers Migration Assistant, and for most users, it’s brilliant. It copies apps, preferences, and user data seamlessly. But for engineers? It’s a minefield.
The Migration Problem: Why Migration Assistant Isn’t Enough
Migration Assistant doesn’t understand dev toolchains. It copies /usr/local and ~/Library blindly, which breaks immediately on Apple Silicon because Homebrew changed its prefix to /opt/homebrew. Rosetta 2 translates most x86_64 binaries flawlessly, but adds ~10–30% overhead, fails for native C-extensions, and doesn’t touch architecture-specific caches or virtual environments.
Suddenly, your python venvs are broken, node global packages point to missing native addons, terraform plugins need reinitialization, and your shell profile references binaries that no longer exist. You’re left with two choices:
- Spend a weekend manually reinstalling, debugging, and reconfiguring everything.
- Hope the migration “just works” and deal with cryptic errors six months later.
I chose neither. I wanted a system where my environment wasn’t a collection of imperative shell commands, but a single, version-controlled source of truth.
Enter Nix: Declarative, Reproducible, Cross-Arch
Nix is fundamentally a package manager, but treating it as just that undersells what it enables. Out of the box, Nix excels at declaratively installing binaries, resolving dependencies, and keeping them isolated. It solves the “how do I install this tool consistently across machines?” problem. But my real headache wasn’t just about fetching software — it was the sprawling, fragile ecosystem of user-level configuration: dotfiles, shell aliases, editor plugins, language runtimes, IDE settings, and environment variables. Scattered across ~/.config, ~/.zshrc, and hidden caches, this state was impossible to version-control or reproduce cleanly.
That’s where home-manager comes in. While Nix manages the packages, home-manager manages the user environment. It’s a Nix-based tool that declaratively configures your home directory: it symlinks config files, wires shell profiles, sets up editor plugins, manages environment variables, and ties application-specific settings together. By combining flakes, nixpkgs, and home-manager, I can define my entire toolchain and personal configuration in a single, version-controlled flake.nix. Then, on any machine:
git clone my-dotfiles.git
cd my-dotfiles
nix run .#homeConfigurations.<username>.activationPackage
That’s it. Same tools. Same versions. Same configs. On Intel Mac, ARM Mac, x86 Linux, or a Raspberry Pi.
How I Actually Use It
Here’s a stripped-down version of my real flake.nix. It uses nixpkgs + home-manager in standalone mode (no NixOS required), pins exact versions, and explicitly supports multiple architectures.
{
description = "My declarative dev environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, home-manager, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in {
homeConfigurations = {
jdoe = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inherit system; };
modules = [ ./home.nix ];
};
};
}
);
}
And home.nix:
{ pkgs, system, ... }:
let
# Cross-arch helper: pick packages based on system
isDarwin = pkgs.stdenv.hostPlatform.isDarwin;
isLinux = pkgs.stdenv.hostPlatform.isLinux;
in
{
# Core shell & editor
home.packages = with pkgs; [
git
neovim
zsh
starship
ripgrep
fd
jq
yq
# Dev toolchains
nodejs_20
python311
go
terraform
docker-compose
# macOS/Linux specific
(if isDarwin then mas else null)
(if isLinux then gcc else null)
];
# Shell configuration
programs.zsh = {
enable = true;
oh-my-zsh = {
enable = true;
plugins = [ "git" "zsh-autosuggestions" "zsh-syntax-highlighting" ];
};
shellAliases = {
gs = "git status";
ga = "git add -A";
gc = "git commit -m";
};
};
# Editor setup
programs.neovim = {
enable = true;
defaultEditor = true;
vimAlias = true;
plugins = with pkgs.vimPlugins; [
telescope-nvim
nvim-treesitter
];
};
# Environment variables
home.sessionVariables = {
EDITOR = "nvim";
LANG = "en_US.UTF-8";
};
# Git config
programs.git = {
enable = true;
userName = "John Doe";
userEmail = "john@example.com";
extraConfig = {
init.defaultBranch = "main";
core.editor = "nvim";
};
};
# Home directory state
home.stateVersion = "24.11";
}
Why This Works for Me
- Pinned versions:
nixpkgs/nixos-24.11+release-24.11means no surprise updates breaking my workflow. - Cross-arch aware:
flake-utils.lib.eachDefaultSystem+pkgs.stdenv.hostPlatformlets me conditionally includemas(macOS App Store CLI) orgccwithout breaking Linux builds. - Idempotent: Run it twice, same result. No duplicate installs, no stale configs.
- Git-tracked: My environment lives in a repo. Code review my dotfiles? Sure.
Real Talk: It’s Not Magic (Yet)
Nix didn’t save me time on day one. It cost me a weekend learning:
- The Nix expression language
- How flakes resolve dependencies
- Debugging
home-manageractivation errors - Understanding why
brew installandnix installdon’t play nice together
And there are caveats:
- Not every package is available on every architecture. Sometimes you need
overrideAttrsor fetch from GitHub. home-managermanages user-space tools. System-level services (Docker daemon, networking, firewall) requirenix-darwinor NixOS.- You’ll still need to migrate GUI apps manually. Nix excels at CLI/toolchain reproducibility, not App Store or
.appbundles.
But the ROI compounds. Every new machine, every OS upgrade, every homelab node now takes < 10 minutes to reach parity. The mental overhead of “will this break after the next macOS update?” is gone.
Final Thoughts
If you’re a developer with a sprawling toolchain, migrating between architectures, or planning a homelab, Nix isn’t just a package manager. It’s an insurance policy against environment drift.
It asks for upfront investment in learning and refactoring. But in return, it gives you something rare in engineering: predictability.
Your machine becomes a function. Input: flake.nix. Output: your workspace. Run it anywhere.
메타데이터
- post_id
- 0ac188497ecf
- slug
- from-migration-anxiety-to-one-command-setups-why-i-bet-my-dev-environment-on-nix-0ac188497ecf
- url
- https://medium.com/@ganesh3075/from-migration-anxiety-to-one-command-setups-why-i-bet-my-dev-environment-on-nix-0ac188497ecf
- canonical_url
- https://medium.com/@ganesh3075/from-migration-anxiety-to-one-command-setups-why-i-bet-my-dev-environment-on-nix-0ac188497ecf
- author_url
- https://medium.com/@ganesh3075
- status
- ok
- fetched_at
- 2026-06-13 00:08:42