Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mike-rambil/9fa7f035380cf4f0b1d0ab5449c48125 to your computer and use it in GitHub Desktop.

Select an option

Save mike-rambil/9fa7f035380cf4f0b1d0ab5449c48125 to your computer and use it in GitHub Desktop.
how_to_setup_project_dev_env_with_NIX

how_to_setup_project_dev_env_with_NIX

Short answer: use Nix + direnv (nix-direnv) + flakes. Result: when you cd into a project, your shell auto-loads the exact tools for that repo; when you delete the folder, its env stops existing and the packages get garbage-collected. No more “uhh which package manager did I use for this?”

Here’s the playbook—fast, opinionated, and minimal chaos.


0) Install the bits (on Arch)

# Nix (multi-user). This is the official script; it's fine.
sh <(curl -L https://nixos.org/nix/install) --daemon
# direnv + nix integration (Arch repos)
sudo pacman -S direnv

# Add direnv to your shell (zsh for you)
nvim ~/.zshrc

Paste this into ~/.zshrc and save:

# --- direnv hook (auto-load per-directory envs) ---
eval "$(direnv hook zsh)"

Then:

# Enable nix-direnv integration (lets direnv understand flakes)
nix-env -iA nixpkgs.nix-direnv

1) Per-project auto-env: .envrc + flake.nix

In your project root, create .envrc:

# Allow direnv in this directory (one-time per dir):
direnv allow

nvim .envrc:

# Use nix flakes to define the dev shell for this project.
# This automatically loads when you `cd` here and unloads when you leave.
use flake

Create flake.nix (commented)

nvim flake.nix:

{
  description = "Per-project dev environment (reproducible, auto-loaded)";

  inputs = {
    # Nixpkgs channel. Pin a known-good revision if you want.
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    # Optional: devshell helpers
    devshell.url = "github:numtide/devshell";
  };

  outputs = { self, nixpkgs, devshell }:
    let
      # Pick your system; change if you're on aarch64, etc.
      system = "x86_64-linux";

      pkgs = import nixpkgs {
        inherit system;
        # Optional: allow unfree if you need proprietary stuff
        # config.allowUnfree = true;
      };
    in
    {
      # `nix develop` or `direnv` will enter this shell.
      devShells.${system}.default = pkgs.mkShell {
        # --- Shell-wide env vars (project-specific) ---
        # e.g., export DATABASE_URL="postgres://..."
        # NIX_CFLAGS_COMPILE = "-O2 -pipe";

        # --- Tools you want available exactly in this project ---
        buildInputs = [
          # Core CLIs
          pkgs.git
          pkgs.curl
          pkgs.just          # task runner (nice to have)

          # Languages / Toolchains
          pkgs.nodejs_20
          pkgs.pnpm
          pkgs.python312
          pkgs.poetry
          pkgs.go
          pkgs.rustup
          pkgs.pkg-config

          # Common libs for building (adjust per project)
          pkgs.openssl
          pkgs.zlib
          pkgs.cmake
          pkgs.gnumake
        ];

        # --- Post-activation hooks (runs on shell enter) ---
        shellHook = ''
          # Keep it obvious you’re in the project env
          echo "[nix] dev env activated for ${PWD##*/}"

          # Node: enable Corepack if you lean yarn/pnpm lockfiles
          if command -v corepack >/dev/null 2>&1; then
            corepack enable >/dev/null 2>&1 || true
          fi

          # Rust toolchain per project (optional)
          rustup default stable >/dev/null 2>&1 || true
        '';
      };
    };
}

How this feels day-to-day:

  • cd my-repo → direnv detects .envrc → loads flake dev shell → all tools in PATH.
  • cd .. → it unloads. No host pollution, no “which package manager did Past You use?”

2) “Automatic uninstall” when you delete the project

  • Nix stores project env builds in the nix store but they’re only kept alive by GC roots (direnv/nix-direnv makes ephemeral roots under ~/.cache/nix/direnv).
  • Delete the project directory → you’ve effectively removed its root.
  • Run garbage collection to reclaim space:
# Manual GC (reclaim dead packages & generations)
nix-collect-garbage --delete-old

# Also clean old direnv shells
nix-direnv prune

Optional: automate GC with a systemd timer (Arch)

Create a weekly GC timer so you never think about it again.

nvim ~/.config/systemd/user/nix-gc.service:

[Unit]
Description=Nix Garbage Collection (user)

[Service]
Type=oneshot
ExecStart=/usr/bin/env nix-collect-garbage --delete-old

nvim ~/.config/systemd/user/nix-gc.timer:

[Unit]
Description=Weekly Nix GC (user)

[Timer]
OnCalendar=weekly
Persistent=true

[Install]
WantedBy=timers.target

Enable it:

systemctl --user daemon-reload
systemctl --user enable --now nix-gc.timer

Now when you trash a repo, its env will get collected automatically on the next run.


3) Quality-of-life (so it’s actually nice)

  • Per-repo language managers still work inside the dev shell, but try to keep the shell authoritative (pin versions here).
  • Lock the world: keep flake.lock in git so teammates get identical tools.
  • Project bootstrap: add a justfile:

nvim justfile:

# Comments because Future You is forgetful.
default: # Show help
    just --list

setup: # First-time project setup
    # e.g., pnpm install or poetry install; runs inside the nix shell
    pnpm install
    poetry install --no-root

4) Bonus: make it zero-braincell to start new projects

Create a tiny template repo you can copy:

gh repo create devshell-template --private
# Drop your flake.nix, .envrc, justfile in there

Then for any new project:

cp -r ~/code/devshell-template new-project
cd new-project
direnv allow

TL;DR

  • When? Every time you cd into a project (thanks to direnv).
  • How? Put a flake.nix (or shell.nix) + .envrc in the repo.
  • Cleanup? Delete the project → run GC (or let your systemd timer do it) → packages are gone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment