Dev tools#

A developer workstation on NixOS: editors, shells, containers, VMs, per-project environments, IDEs, and the “how do I run this random binary from the internet” problem.

This page assumes you already have a real NixOS host — NixOS-WSL, a VirtualBox sandbox, a libvirt VM on Kali, or real hardware. If you are still at the home-manager-on-Debian / home-manager-on-Kali stage (Level 2a), most of what’s below has a home-manager equivalent; see home-manager-modes.md. The virtualisation.*, programs.nix-ld, programs.docker.* options shown here are NixOS-only and require configuration.md to be your starting point.

In this page#

System-wide CLI tools#

environment.systemPackages = with pkgs; [
  # editors
  neovim
  micro
  helix

  # shell utilities
  git
  git-lfs
  tmux
  zellij
  fzf
  ripgrep
  fd
  bat
  eza
  zoxide
  jq
  yq
  httpie
  curl
  wget

  # system
  btop
  htop
  ncdu
  dust
  duf
  lsof
  tree
  file
  unzip
  zip
  p7zip

  # nix-specific
  nix-tree         # explore store closures
  nix-output-monitor  # `nom` — prettier `nix build`
  nvd              # diff generations
  nix-index        # find which package provides a command
  nixpkgs-review
];

Shells#

programs.fish.enable = true;
programs.zsh.enable = true;

users.users.alice.shell = pkgs.fish;

Per-user fish config goes in ~/.config/fish/config.fish — or declaratively via home-manager’s programs.fish.* options.

Starship prompt is system-wide via programs.starship.enable = true; but most users prefer declaring it per-user through home-manager.

Git / pre-commit hooks#

programs.git = {
  enable = true;
  lfs.enable = true;
  config = {
    init.defaultBranch = "main";
    pull.rebase = true;
    push.autoSetupRemote = true;
  };
};

Pre-commit ships as pre-commit in nixpkgs; use it via direnv in per-project shells, not system-wide.

Docker / Podman#

virtualisation.docker = {
  enable = true;
  rootless = {
    enable = true;
    setSocketVariable = true;
  };
};

# Add your user to the docker group for rootful socket access
users.users.alice.extraGroups = [ "docker" ];

Podman as a drop-in with the same CLI:

virtualisation.podman = {
  enable = true;
  dockerCompat = true;       # symlinks `docker` → `podman`
  defaultNetwork.settings.dns_enabled = true;
};

Pick one daemon per host. If you are coming from Docker Desktop on Windows (WSL2 backend) or Docker Desktop on macOS, the native Linux daemon enabled above replaces that shim entirely — no Desktop client, no VM-in-a-VM. From inside NixOS-WSL the daemon runs inside the WSL2 distro directly, so interop with Windows containers is lost; that is nearly always fine for Linux-container development.

Virtualization (virt-manager / libvirt / QEMU)#

virtualisation.libvirtd = {
  enable = true;
  qemu = {
    package = pkgs.qemu_kvm;
    swtpm.enable = true;
    ovmf = {
      enable = true;
      packages = [ pkgs.OVMFFull.fd ];
    };
  };
};

programs.virt-manager.enable = true;
users.users.alice.extraGroups = [ "libvirtd" "kvm" ];

Now virt-manager can create Windows/Linux VMs with full UEFI + TPM support.

Per-project environments#

The golden pattern on NixOS: one shell.nix (or flake.nix) per repo, auto-activated by direnv.

Enable direnv system-wide#

programs.direnv = {
  enable = true;
  nix-direnv.enable = true;   # fast, caching-aware Nix integration
  loadInNixShell = true;
};

Then in any repo:

echo "use flake" > .envrc        # for flakes
# or
echo "use nix"   > .envrc        # for classic shell.nix
direnv allow

Minimal shell.nix#

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  packages = with pkgs; [ python312 uv ruff pre-commit ];
  shellHook = ''
    export PYTHONDONTWRITEBYTECODE=1
  '';
}

Minimal flake.nix devShell#

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  outputs = { nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      devShells.${system}.default = pkgs.mkShell {
        packages = with pkgs; [ python312 uv ruff pre-commit ];
      };
    };
}

devenv / flox / pixi on top of Nix#

All three run on NixOS the same as they do on Debian. See python/setup for the devenv + direnv workflow already documented in this repo. On NixOS the install step simplifies — devenv is just a package:

environment.systemPackages = with pkgs; [ devenv ];

IDEs#

environment.systemPackages = with pkgs; [
  # editors / IDEs
  vscode                   # unfree
  vscode-fhs               # vscode in an FHS env — fixes native extensions that `patchelf`-reject
  jetbrains.idea-community
  jetbrains.pycharm-community
  jetbrains.clion          # unfree
  zed-editor
];

nixpkgs.config.allowUnfree = true;

Cursor is packaged as code-cursor in unstable, or install as an AppImage — see packages.md.

Language toolchains and the FHS caveat#

Pure nixpkgs packages work perfectly. Where you hit friction is when a language’s own package manager tries to install prebuilt binaries (pip wheels with native libs, npm/pnpm native modules, rustup-installed toolchains, etc.). The dynamic linker on NixOS is not at /lib64/ld-linux-x86-64.so.2, so those binaries fail with confusing errors.

Three fixes, from cleanest to easiest:

  1. Use nixpkgs’s own version:

    environment.systemPackages = with pkgs; [
      python312
      nodejs_22
      rustc cargo
      go
      jdk21
    ];
    
  2. Use programs.nix-ld to fake an FHS dynamic linker for foreign binaries. The canonical library list lives in packages.md; declare it once in a shared module and import from every host. A minimal working set:

    programs.nix-ld = {
      enable = true;
      libraries = with pkgs; [
        stdenv.cc.cc.lib
        zlib zstd
        openssl
        curl
        glib
        libxkbcommon
        icu
        fuse3
        xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr xorg.libXxf86vm
        libGL
      ];
    };
    

    Now prebuilt binaries (VSCode extensions, pip wheels, pnpm/npm native modules, rustup-installed toolchains, etc.) mostly Just Work. When one still fails, the error names the missing lib<foo>.so.N — add the corresponding nixpkgs package to libraries and rebuild.

  3. Run inside an FHS shell for the stubborn cases:

    (pkgs.buildFHSEnv {
      name = "fhs-dev";
      targetPkgs = p: with p; [ gcc zlib openssl ];
      runScript = "bash";
    })
    

    Launch with the command name; inside, paths behave like Ubuntu/Debian.

See packages.md for the same pattern applied to games and random binaries.

Secrets in dev (quick note)#

Don’t put secrets in configuration.nix — it lands in the world-readable /nix/store. Use direnv-local .envrc (gitignored) during dev. For production-style secrets-in-config, see the agenix/sops-nix pointer in snippets.md.

Next#

  • packages.md — adding packages, unfree, Steam, AppImages, Flatpak, nix-ld canonical library list.

  • home-manager-modes.md — putting programs.fish/programs.git/programs.direnv in home.nix instead.

  • multi-host-flake.md — sharing one dev.nix module across every host.

  • snippets.md — docker rootless, GPU, fonts, etc.