Overlays (light introduction)

Updated

July 30, 2026

Overlays (light introduction)

Goal: Apply a small overlay so the same package change is visible to the NixOS host and to devShells—without forking nixpkgs or cargo-culting overlay stacks.

Important

Overlays are sharp tools. Prefer options, package overrides in a single module, or upstream pins when they suffice. This chapter teaches the mechanism so you are not helpless—not so every problem becomes an overlay.

Why this chapter exists

Sooner or later you need:

  • A newer/older minor of one tool than nixpkgs 26.05 ships
  • A one-line patch to a package you use
  • A renamed or wrapped package for your lab

Overlays let you modify the pkgs fixed-point. Done lightly, they are elegant. Done heavily, they make upgrades impossible.


Theory 1 — What an overlay is

An overlay is a function:

final: prev: {
  # new or overridden attrs
}
Argument Meaning
prev Package set before this overlay
final Package set after all overlays (fixed-point)—use for deps on other overridden pkgs
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    # example: add a patch or change configure flags
  });
}

Fixed-point intuition

pkgs = fix (self: vanilla // overlay1 self vanilla // overlay2 self …)

You rarely write fix yourself; Nixpkgs applies overlays for you.


Theory 2 — Wiring overlays on NixOS

# modules/common/nixpkgs.nix
{ ... }:
{
  nixpkgs.overlays = [
    (import ../../overlays/hello-lab.nix)
    # or inline:
    # (final: prev: { ... })
  ];
}

With home-manager.useGlobalPkgs = true, HM sees the same overlayed pkgs. That is why the Home Manager chapter set that flag.

Flake-level pkgs for shells/checks

pkgsFor = system: import nixpkgs {
  inherit system;
  overlays = [
    (import ./overlays/hello-lab.nix)
  ];
};

Consistency rule: the overlay list used for nixosConfigurations should match the list used for devShells / checks when you care about the same tools—or document intentional differences.


Theory 3 — overrideAttrs vs override vs new package

Tool When
pkg.override { ... } Change callPackage parameters
pkg.overrideAttrs Change derivation attributes (patches, version, src)
New attr final.mytool = ... Add a package name without clobbering

Example patch (illustrative):

# overlays/hello.nix
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    patches = (old.patches or []) ++ [
      # ./patches/hello-example.patch
    ];
  });
}

If you do not have a real patch, use a harmless visible change—or prefer adding a wrapper package instead of mutating hello:

final: prev: {
  hello-lab = prev.writeShellScriptBin "hello-lab" ''
    echo "lab overlay wrapper"
    exec ${prev.hello}/bin/hello "$@"
  '';
}

Wrappers are often safer than overriding core packages.


Theory 4 — Composition order

nixpkgs.overlays = [ overlayA overlayB ];

Later overlays see earlier results via prev chain. Conflicts: last writer wins for the same attr. Keep one file per concern and a short list.

# overlays/default.nix
[
  (import ./hello-lab.nix)
  # (import ./mypackage.nix)
]
nixpkgs.overlays = import ../../overlays/default.nix;

Theory 5 — When not to overlay

Need Prefer
Enable a service option NixOS module option
Package only for one project Project devShell / packages
Temporary try nix shell / branch pin
Huge fork of desktop stack Separate nixpkgs input (advanced)
One CLI flag Wrapper script in module

Overlays that rewrite half of python3Packages are packaging-part territory—and often a mistake for hosts.


Theory 6 — Pinning a package from another nixpkgs (light)

Sometimes you want one package from a newer pin without moving the whole OS:

# advanced sketch — use sparingly
# inputs.nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
# final: prev: {
#   fancy = inputs.nixpkgs-unstable.legacyPackages.${prev.system}.fancy;
# }

Costs: two package graphs, closure size, ABI surprises. In this chapter, a wrapper or small overrideAttrs is enough.


Theory 7 — Overlays vs packageOverrides (classic)

Older NixOS configs used nixpkgs.config.packageOverrides. Prefer nixpkgs.overlays in modern flakes. If you find both, consolidate.


Theory 8 — Infinite recursion patterns

Anti-pattern Why
final.foo = final.foo.override… carelessly Wrong fixed-point use
Overlay imports a module that imports pkgs that need the overlay Cycles
Using final where prev suffices Accidental recursion

Rule of thumb: take base package from prev; reach for final only when depending on another new attr you defined in the same overlay set.

final: prev: {
  myLib = prev.callPackage ./mylib { };
  myApp = prev.callPackage ./myapp { inherit (final) myLib; };
}

Worked example — Shared overlay module

overlays/
  default.nix      # composes list
  hello-lab.nix
modules/common/nixpkgs.nix
# overlays/default.nix
[
  (import ./hello-lab.nix)
]
# overlays/hello-lab.nix
final: prev: {
  hello-lab = prev.writeShellScriptBin "hello-lab" ''
    echo "hello from lab overlay"
    exec ${prev.hello}/bin/hello "$@"
  '';
}
# modules/common/nixpkgs.nix
{ ... }:
{
  nixpkgs.overlays = import ../../overlays/default.nix;
}

In a module that receives pkgs:

{ pkgs, ... }:
{
  environment.systemPackages = [ pkgs.hello-lab ];
}

And in flake pkgsFor, import the same overlay list for shells/checks.


Worked example — HM sees the overlay

# homes/alice/programs.nix fragment
{ pkgs, ... }:
{
  home.packages = [ pkgs.hello-lab ];
}

Requires home-manager.useGlobalPkgs = true.


Exercises

Exercise 1 — Create overlay file

mkdir -p overlays
# add hello-lab.nix and default.nix as above

Exercise 2 — Attach to NixOS

Import overlay list via nixpkgs.overlays. Add pkgs.hello-lab to system or HM packages. Rebuild:

sudo nixos-rebuild switch --flake .#lab
hello-lab

Exercise 3 — Attach to devShell / checks pkgs

Ensure pkgsFor (or flake-parts pkgs) uses the same overlays.

nix develop -c hello-lab

If only system has it, fix the flake pkgs construction.

Exercise 4 — Prove HM sees it (if useGlobalPkgs)

home.packages = [ pkgs.hello-lab ];

Rebuild; as user command -v hello-lab.

Exercise 5 — Intentional override discipline

Journal:

  1. What you overlaid/wrapped and why
  2. How you will remove it when upstream catches up
  3. Whether a project-only shell would have been enough

Exercise 6 — Negative space

Find one change you could do with an overlay but will do with a module option instead. Write it down—builds judgment.

Exercise 7 — Composition list

Add a second tiny wrapper (cowsay-lab or similar). Confirm both appear. Practice ordering in overlays/default.nix.

Exercise 8 — Failure: overlay not wired

Temporarily remove overlay from pkgsFor only; observe shell missing package while system has it. Restore consistency.

Exercise 9 — Document overlays

Add docs/overlays.md: list each overlay, purpose, removal criteria.

Exercise 10 — Check integration

If you have flake checks, add a check that builds pkgs.hello-lab or run it in the shell check path.

Exercise 11 — Prefer wrapper over override

If you mutated hello, convert to hello-lab wrapper instead. Rebuild.

Exercise 12 — Search before overlay

For one real need, search nixpkgs options / HM options first. Only overlay if no option exists.

Exercise 13 — Recursion caution note

Write two sentences in docs/overlays.md about final vs prev.

Exercise 14 — Upgrade readiness

When nixpkgs updates, re-run hello-lab and note if still needed.


Common gotchas

Symptom / mistake What to do
pkgs.hello-lab missing Overlay not wired into that pkgs import
HM doesn’t see overlay useGlobalPkgs false or separate pkgs
Infinite recursion in overlay Using final incorrectly; simplify
Overlay only on system Wire flake pkgsFor too
Overlaying all of nixpkgs for fun Revert; minimize surface
Patch path wrong Path relative to overlay file / flake root
Two competing overlay lists One source of truth
Clobbering upstream attr accidentally Prefer new attr names
Project-only need in host overlay Move to project flake
Forgot removal plan Document in docs/overlays.md

Checkpoint

  • Overlay or wrapper package defined in repo
  • Visible on NixOS host packages
  • Visible in devShell or documented intentional difference
  • HM consistency verified if applicable
  • Removal plan written
  • Judgment note: when not to overlay
  • Single overlay composition list
  • Docs list active overlays

Journal (optional)

Write three personal gotchas before continuing.


Part close-out

You now have:

Capability Chapter
Scalable repo layout Flake repository layout
Deliberate pins Lock file discipline
User plane via HM module Home Manager + files + services
Project toolchains direnv + devShells
Multi-output style flake-parts or hand-rolled
Quality gates Flake checks
Light package tweaks Overlays

Next part: Services and security patterns—secrets, hardening, firewall, TLS, data services, containers, observability—on top of this shape.