Let me tell you about my development setup that's been a game-changer for my productivity. I've combined Nix flakes for rock-solid package management, Ansible for system orchestration, and GNU Stow for dotfiles that actually let me iterate quickly.
This isn't just another "here's my dotfiles" repo—it's a hybrid approach that gives me reproducible environments without sacrificing the developer experience I actually want.
Here's the thing about pure Nix home-manager: it's theoretically perfect and practically frustrating. I tried it, lived with it for months, and finally admitted what every developer knows deep down—sometimes you just want to edit a config file and see the change immediately.
Nix handles my packages because:
- I get the exact same
ripgrepversion on my MacBook and my Linux servers - Dependencies just work (no more "works on my machine" nonsense)
- I can rollback when that new Node version breaks everything
- Zero platform-specific package management headaches
Stow handles my dotfiles because I actually develop:
- When I tweak my Neovim config, I want to
:source %and see it work - My tmux settings should reload with
prefix + r, not a full system rebuild - Shell aliases need to be testable with a quick
source ~/.zshrc - I experiment with configs constantly—rebuild cycles kill creativity
The immutable dotfiles trap: Nix home-manager sounds great until you're waiting for a rebuild every time you want to test a one-line config change. That's not development—that's bureaucracy.
My solution is embarrassingly simple:
- Nix: Handles packages (the stuff that should be immutable)
- Stow: Handles dotfiles (the stuff I actually edit)
- Ansible: Ties it all together and manages my secrets
Result? I get reproducible environments that don't fight me when I'm trying to work.
- Actually works everywhere: Same setup on my MacBook, Linux desktop, and every server I touch
- No more platform hell: One config file, not separate scripts for apt/brew/pacman/dnf
- ARM support: Because M1 Macs exist and Linux ARM servers are getting popular
- SSH that just works: Automated agent setup, no more ssh-add dance every reboot
- Instant config iteration: Edit, reload, test—the way development should be
- Secure secrets: Ansible vault keeps my SSH keys encrypted, not sitting in plain text
- Real testing: Docker containers that verify everything actually works
- Search & Navigation: fzf, ripgrep (rg), bat, fd, zoxide
- Network & Archives: wget, curl, unzip
- Development: tmux, jq, git-delta, gh (GitHub CLI), git-lfs, stow
- Languages: go, nodejs, python3, fnm (Node version manager), uv (Python package manager)
- Build Tools: cmake, ninja, gettext
- Security: openssh, sops, age
- Shell: zsh with starship prompt
- Fonts: Hack and Fira Code Nerd Fonts
- Editor: neovim
- SSH: Automated SSH agent with key management
Three things, and you probably already have one of them:
- Nix with flakes - The magic that makes this work everywhere
- Ansible - Handles the orchestration
- Git - Because this is 2024
macOS:
# Install Nix
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# Install Ansible
pip install ansibleLinux (any distribution):
# Install Nix
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# Install Ansible (use your distribution's package manager)
# Ubuntu/Debian: apt install ansible
# Arch: pacman -S ansible
# Fedora: dnf install ansible# Get the Ansible bits we need
ansible-galaxy collection install community.general
# Do everything at once
# (You'll need your sudo password and vault password)
make allThat's it. Seriously. Go grab coffee while it sets up your entire development environment.
# Just the Nix packages
make nix
# Shell setup (makes zsh your default)
make zsh
# SSH keys and configuration
make ssh
# Basic git setup
make git-setup
# Deploy your actual dotfiles
make dotfiles
# Test that everything works
make test- flake.nix: Nix flake managing all development packages and SSH agent configuration (dynamically customized via Ansible)
- local.yml: Main Ansible playbook that orchestrates the entire setup
- tasks/: Directory containing streamlined task modules:
nix.yml: Nix installation via Determinate Systems installer and flake.nix customization (replaces platform-specific package management)zsh.yml: System-level ZSH shell configuration and cleanupssh.yml: SSH key management via Ansible vaultgit-setup.yml: Basic Git configuration and host key setupdotfiles.yml: Dotfiles deployment using GNU stow
- ansible.cfg: Ansible configuration with timer and profiling callbacks
- Makefile: Convenient command shortcuts with streamlined targets
- flake.lock: Nix flake lock file ensuring reproducible package versions
- requirements.yml: Ansible collections for cross-platform support
# Test multi-user template system (tests dynamic flake.nix customization)
./test/test-nix.sh multiuser
# Test full Nix installation via Ansible in container
./test/test-nix.sh test
# Interactive testing in Arch Linux container
./test/test-nix.sh interactive
# Build test container only
./test/test-nix.sh build
# Or use the Makefile shortcut
make testThe test infrastructure validates:
- Template system: Multi-user flake.nix customization works correctly
- Container isolation: Nix installation via Ansible in clean environments
- Package availability: All development tools are accessible after setup
- Cross-platform compatibility: Same setup works on different architectures
- Nix gets installed via the Determinate Systems installer (it's the good one)
- flake.nix gets customized with your user details (git name, email, home directory paths)
- My flake activates and suddenly you have all my development tools
- SSH agent starts working automatically (no more manual ssh-add)
- System gets configured with sane defaults for git, shell, etc.
- Your dotfiles deploy and everything Just Works™
Instead of using Jinja2 templates, this setup uses a smarter approach:
- Single source of truth: One
flake.nixfile that's readable and maintainable - Dynamic customization: Ansible replaces user-specific values (usernames, emails, paths) at deployment time
- No template files: No separate
.j2files to maintain or keep in sync - Multi-user support: Same flake works for any user with their specific configuration
- Write once, run anywhere: No more separate configs for every OS
- Development at the speed of thought: Edit configs, see results instantly
- Reproducible but not rigid: Consistency where it matters, flexibility where you need it
- Security that doesn't suck: Encrypted secrets that don't get in your way
- Built for people who actually code: Hot-reload everything, rebuild nothing
My setup includes automated SSH agent configuration:
- Nix home-manager creates systemd user service for SSH agent
- Creates socket at
$XDG_RUNTIME_DIR/ssh-agent.socket - My dotfiles can reference this with:
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket" - SSH keys automatically added to agent when first used
The setup includes ZSH autosuggestions plugin managed via Nix with Stow-based configuration:
- Nix downloads:
zsh-autosuggestionspackage managed by Nix for reproducibility - Stow configures: Your
.zshrc(from dotfiles) sources the Nix-provided plugin - Hybrid approach: Best of both worlds - reliable downloads, flexible configuration
Add this to your .zshrc in your dotfiles repository to source the Nix-provided autosuggestions:
# Source ZSH plugins provided by Nix
if [ -f ~/.nix-profile/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then
source ~/.nix-profile/share/zsh-autosuggestions/zsh-autosuggestions.zsh
fiAfter updating your dotfiles repository, run make dotfiles to apply changes via Stow.
The flake includes automated keyd configuration for keyboard remapping on Linux systems:
- Automatic setup: keyd package and systemd user service configured automatically
- User-level service: Runs as a user service, no root privileges needed after setup
- Configuration: Uses your dotfiles keyd configuration from
~/.dotfiles/keyd/etc/keyd/default.conf - Current mapping: Caps Lock → Escape (tap) or Ctrl (hold), Escape → Caps Lock
# Check service status
systemctl --user status keyd
# Restart after config changes
systemctl --user restart keyd
# View service logs
journalctl --user -u keyd
# Stop/start service
systemctl --user stop keyd
systemctl --user start keydAfter editing your keyd configuration file (~/.dotfiles/keyd/etc/keyd/default.conf), restart the service to apply changes:
systemctl --user restart keydThe keyd service starts automatically with your desktop session and requires no manual intervention.
- SSH keys and sensitive dotfiles are encrypted with Ansible vault
- Use
--ask-vault-passflag for tasks requiring vault access - Store vault password securely in password manager
- For convenience, create
.vault-passfile (add to.gitignore)
Since this setup uses Nix home-manager for package management, here's what you need to know:
# Activate home-manager configuration (install/update packages)
nix run .#homeConfigurations.wesbragagt@linux-x86_64.activationPackage
# Check what packages are currently installed
home-manager packages
# Search for available packages
nix search nixpkgs <package-name>
# See what would be installed/updated (dry run)
nix run .#homeConfigurations.wesbragagt@linux-x86_64.activationPackage --dry-run# Edit the home-manager configuration
# Configuration is defined in flake.nix under home-manager.users.wesbragagt
# View current home-manager generation
home-manager generations
# Rollback to previous generation
home-manager rollback
# Remove old generations (cleanup)
home-manager expire-generations "-30 days"# List all home-manager services
systemctl --user list-units --type=service | grep -E "(home-manager|nix)"
# Check SSH agent service (managed by home-manager)
systemctl --user status ssh-agent
# Restart all home-manager services
systemctl --user restart home-manager-*.service# Drop into a shell with everything available
nix develop
# Check if your changes are valid before applying
nix flake check
# Update to latest packages
nix flake update
# See what this flake actually provides
nix flake show- My Configuration: Defined in
flake.nixunderhome-manager.users.wesbragagt - State directory:
~/.local/state/nix/profiles/home-manager - Service logs:
journalctl --user -u home-manager-*.service
# If nix command not found, source the profile
source ~/.nix-profile/etc/profile.d/nix.sh
# or for multi-user installs:
source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
# Update flake dependencies
nix flake update
# Check flake structure
nix flake show# Check SSH agent status
systemctl --user status ssh-agent
# Restart SSH agent service
systemctl --user restart ssh-agent
# Check SSH agent socket
ls -la $XDG_RUNTIME_DIR/ssh-agent.socket# Search for packages in nixpkgs
nix search nixpkgs <package-name>
# Enter development shell to test packages
nix develop
# Rebuild home-manager configuration
nix run .#homeConfigurations.wesbragagt@linux-x86_64.activationPackage