Comparing changes in a Nix Flake

Nix/NixOS is a declarative language for defining your entire operating system. I use it on my dedicated servers to be able to apply GitOps for the servers. I define my services in a Git repo, everything from what version of packages to use, to what services should be installed, and how they should be installed. Those servers run Kubernetes which is where most of my services live.

Nix is a beast. The language is quite complicated and I wrote about my challenges. While I’ve gotten used to the language, I still don’t consider it intuitive. With that out of the way, my next challenge is that if I run nix flake update, which updates the packages that come from NixPkgs (which is most packages that you install.) Then I don’t really know what’s changing.

The Problem

Every time I run nix flake update, I end up with a diff that looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
diff --git a/flake.lock b/flake.lock  
index 125bbcf..c122eb0 100644  
--- a/flake.lock  
+++ b/flake.lock
@@ -134,11 +137,24 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1780036600,
-        "narHash": "sha256-SSgsnJO46Tjaheiw7TnpQHtjO6rwbQZPbFYY2vLqk8A=",
+        "lastModified": 1767892417,
+        "narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
+        "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
+        "type": "tarball",
+        "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
+      }
+    },
+    "nixpkgs_2": {
+      "locked": {
+        "lastModified": 1781071287,
+        "narHash": "sha256-QacjyL0WxppiT8eqIwzIE836MdhByxq+CVSsDNm3Fkw=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "65b17bdb9959018296a1afc624695168414f4db9",
+        "rev": "f3c00d2737ae70055f5207e2421c46eb5fec7876",
         "type": "github"
       },
       "original": {

This doesn’t convey anything meaningful. Is it a small change? A big change? Am I introducing breaking changes? Are any of my services going to restart?

It’d be great if there was a way to compare the difference in my Nix roots to see what actually changed.

Tool Analysis

There are a few tools that I found that already handle this.

Both tools work by partially building (without actually compiling) the old version and new versions.

1
2
3
4
5
6
HOST="srv9"

NEW_PATH=$(nix path-info --derivation "path:.#nixosConfigurations.${HOST}.config.system.build.toplevel")

# Switch to old version
OLD_PATH=$(nix path-info --derivation "path:.#nixosConfigurations.${HOST}.config.system.build.toplevel")

Comparison of Tools

Tool #1 - dix

1
nix run "github:NixOS/nixpkgs#dix" --inputs-from path:. -- "$OLD_PATH" "$NEW_PATH"

The first tool I tested called nix-diff, gave me a list of changed, added, and removed nixpkgs. Though, it becomes hard to read because Nixpkgs includes numerous changes of both packages that are important (e.g. etcd is changing), mixed in with numerous single patch files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  Old: /nix/store/p4d9k8b9bdlkxb2rnk7xgxl9phfh0p7n-nixos-system-srv9-26.11.20260529.65b17bd.drv
  New: /nix/store/ks9wfpigs9s2qwxxwmp9hfymyy1hcp0i-nixos-system-srv9-26.11.20260614.6f11897.drv
<<< /nix/store/p4d9k8b9bdlkxb2rnk7xgxl9phfh0p7n-nixos-system-srv9-26.11.20260529.65b17bd.drv
>>> /nix/store/ks9wfpigs9s2qwxxwmp9hfymyy1hcp0i-nixos-system-srv9-26.11.20260614.6f11897.drv

CHANGED
...
[U.] etcd                                                                   3.6.11 -> 3.6.12
[U.] etcdctl                                                                3.6.11-go-modules, 3.6.11 -> 3.6.12-go-modules, 3.6.12
[U.] etcdserver                                                             3.6.11-go-modules, 3.6.11 -> 3.6.12-go-modules, 3.6.12
...

ADDED
...
[A.] equivalent                                                             1.0.2
[A.] indexmap                                                               2.14.0
...

REMOVED
[R.] 100-fix-gcc14-build.patch                                              <none>
[R.] 2010_add_build_timestamp_setting.patch                                 <none>
[R.] 20_no_Werror.diff                                                      <none>
[R.] 2251737b3b175925684ec0d37029ff4cb521d302.patch                         <none>
[R.] 30_ag_macros.m4_syntax_error.diff                                      <none>
...
[R.] adns                                                                   1.6.1.tar.gz, 1.6.1
[R.] alsa-lib                                                               1.2.15.3.tar.bz2, 1.2.15.3
[R.] alsa-plugin-conf-multilib.patch                                        <none>
...

PATHS: 4672 -> 4421 (+596, -847)
SIZE: 230 MiB -> 230 MiB
DIFF: 232 KiB

Tool #2 - nix-diff

1
nix run "github:NixOS/nixpkgs#nix-diff" --inputs-from path:. -- "$OLD_PATH" "$NEW_PATH"

The next tool, confusing named nix-diff, works the same way as before–given two Nix derivations paths, it computes the changes between them. Except it visualizes it different. Here’s a flake update:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 /nix/store/p4d9k8b9bdlkxb2rnk7xgxl9phfh0p7n-nixos-system-srv9-26.11.20260529.65b17bd.drv:{out}
+ /nix/store/ryyfi5k2ayl9dzyndhl4accyjajmzvil-nixos-system-srv9-26.11.20260619.27a095f.drv:{out}
 The set of input derivation names do not match:
    - initrd-linux-6.18.33
    - linux-6.18.33
    - linux-6.18.33-modules
    + initrd-linux-6.18.36
    + linux-6.18.36
    + linux-6.18.36-modules
 The input derivation named `activate` differs
  - /nix/store/7jzqzanvshv7m21mj79ajrsk7bajbmvp-activate.drv:{out}
  + /nix/store/zq212jmvldndzacb2ajvf14xpbpqgf4k-activate.drv:{out}
   The input derivation named `bash-interactive-5.3p9` differs
    - /nix/store/sqwn65xb0ymbbhfbvcsksy7py02pd1gd-bash-interactive-5.3p9.drv:{out}
    + /nix/store/qq9ryli5b3babf4nxrv32i62bcr6vihp-bash-interactive-5.3p9.drv:{out}
     The input derivation named `bash-5.3.tar.gz` differs
      - /nix/store/rzkd3k18k5yjhqqq6qn8633za14dz9nw-bash-5.3.tar.gz.drv:{out}
      + /nix/store/3fihvbzw6j53l3xqi5168kkhnxd4a49i-bash-5.3.tar.gz.drv:{out}
       The input derivation named `mirrors-list` differs
        - /nix/store/84w0zzmqzl2bn5s4rx7axsh9znws1rdf-mirrors-list.drv:{out}
        + /nix/store/qkrfwhcrn7af1w11gxh3m45xjsgxlrxz-mirrors-list.drv:{out}

continues for thousands of lines

It can even show the files differences. Here’s an example where I changed a network configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
- /nix/store/p4d9k8b9bdlkxb2rnk7xgxl9phfh0p7n-nixos-system-srv9-26.11.20260529.65b17bd.drv:{out}
+ /nix/store/v563pm583hzk7ghfz1ngv69m0p23p4dp-nixos-system-srv9-26.11.20260529.65b17bd.drv:{out}
 The input derivation named `activate` differs
  - /nix/store/7jzqzanvshv7m21mj79ajrsk7bajbmvp-activate.drv:{out}
  + /nix/store/cm2blp83xb0gkbkkz88w62sgc23f7hi4-activate.drv:{out}
   The input derivation named `etc` differs
    - /nix/store/zra979j9iamynnq6mhjn9rc14mp2mnrn-etc.drv:{out}
    + /nix/store/7j8y5xpdaji14vf7xzrwrp25b474pmkq-etc.drv:{out}
     The input derivation named `system-units` differs
      - /nix/store/0m2yxv4p5g09918c09381c93y292rdcz-system-units.drv:{out}
      + /nix/store/kbpc9238p0g928s30m3kgkdhw4b8xrx8-system-units.drv:{out}
       The input derivation named `unit-systemd-networkd.service` differs
        - /nix/store/bg1p3bxk2yld11llqnspwkn9868i9gps-unit-systemd-networkd.service.drv:{out}
        + /nix/store/s59rxwijqw9j5ysqvigzqbw07ny7ia1s-unit-systemd-networkd.service.drv:{out}
         The input derivation named `X-Reload-Triggers-systemd-networkd` differs
          - /nix/store/ifx8cnd1v84kjp688mcgasnzzkxpaslj-X-Reload-Triggers-systemd-networkd.drv:{out}
          + /nix/store/mgyvv9ysg6zfplllrybpcv08b4zn75ny-X-Reload-Triggers-systemd-networkd.drv:{out}
           The input derivation named `unit-ens18.network` differs
            - /nix/store/kwmg7j0hx45m3mrv0wkl0ang8injvp0n-unit-ens18.network.drv:{out}
            + /nix/store/vh06fqvmnyva7q5hp3sc1jz19wa33m7j-unit-ens18.network.drv:{out}
             The environments do not match:
                text=''
                [Match]
                Name=ens18
                
                [Link]
                RequiredForOnline=routable
                
                [Network]
                Address=15.204.37.15/29←→Address=15.204.37.16/29
                Address=2604:2dc0:200:fa4:beef:beef:beef:beef/80
                
                [Route]
                Destination=15.204.37.15/29←→Destination=15.204.37.16/29
                Scope=link
                Source=15.204.37.15←→Source=15.204.37.16
                
                [Route]
                Destination=51.81.243.254/32
                Scope=link
                Source=15.204.37.15←→Source=15.204.37.16
                
                [Route]
                Destination=2604:2dc0:200:0fff:00ff:00ff:00ff:00ff/64
                Source=2604:2dc0:200:fa4:beef:beef:beef:beef
                
                [Route]
                Gateway=51.81.243.254
                GatewayOnLink=true
                
                [Route]
                Gateway=2604:2dc0:200:0fff:00ff:00ff:00ff:00ff
                GatewayOnLink=true
                
            ''
           Skipping environment comparison
         Skipping environment comparison
       Skipping environment comparison
     Input derivations differ but have already been compared
        unit-ens18.network
     Skipping environment comparison
   Skipping environment comparison
 The input derivation named `boot.json` differs
  - /nix/store/sx2bwx5zgx55ks8mm21mmiambk2igb70-boot.json.drv:{out}
  + /nix/store/8l3qfak4dp05yi30d0iw7w479wq8iad9-boot.json.drv:{out}
   The input derivation named `initrd-linux-6.18.33` differs
    - /nix/store/fqn732pisgd8xcfmgmp1dwr5v7h4ym5c-initrd-linux-6.18.33.drv:{out}
    + /nix/store/3dpp66v2k9dr0vibhxc9p92m7flnqksp-initrd-linux-6.18.33.drv:{out}
     Input derivations differ but have already been compared
        unit-ens18.network
     Skipping environment comparison
   Skipping environment comparison

Executing in a script

When I’m performing nix flake updates, I run the following script to compare the difference between my previous commit and current unstaged changes using a git stash.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash
set -euo pipefail

HOST="${1:-srv9}"

echo "Getting current system derivation..."
NEW_PATH=$(nix path-info --derivation "path:.#nixosConfigurations.${HOST}.config.system.build.toplevel")

echo "Stashing changes and getting previous system derivation..."
git stash
OLD_PATH=$(nix path-info --derivation "path:.#nixosConfigurations.${HOST}.config.system.build.toplevel")
git stash pop

echo "Comparing system between:"
echo "  Old: $OLD_PATH"
echo "  New: $NEW_PATH"

nix run "github:NixOS/nixpkgs#nix-diff" --inputs-from path:. -- --color=never "$OLD_PATH" "$NEW_PATH"

Comparison of tools

Both Nix diffing tools give a huge amount of output for nix flake updates that it can be a overwhelming. While neither really tell me what I need to know (i.e. does Kubernetes get rebuilt or restarted?), they’re still useful to consult. I generally use the dix more frequently for nix flake updates and nix-diff when I’m developing a single derivation.

Integrating with Forgejo Actions Workflows

As part of my Nix GitOps repository, I was already running nix flake check to ensure that my configuration was valid. Next, I want to setup automatically weekly nix flake update operations that create a pull request that I manually review and merge.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
on:
  push:
  pull_request:

jobs:
  build:
    name: Build Nix targets
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
      - name: Check Nix flake inputs
        uses: https://github.com/DeterminateSystems/flake-checker-action@v12

      - name: Build default package
        run: nix flake check --no-update-lock-file

      - uses: https://github.com/natsukium/nix-diff-action@v1.1.0
        if: github.event_name == 'pull_request'
        with:
          attributes: |
            - displayName: srv9
              attribute: nixosConfigurations.srv9.config.system.build.toplevel

With the above workflow, I get comment on each pull request showing the changed packages:

Conclusion

While Nix and NixOS provides strong declarative infrastructure management through GitOps, the developer experience still represents a friction point for me. While I can see a Git diff on flake.lock, there’s no meaningful information in it.

Tools like dix and nix-diff bridge this gap by deriving meaningful comparisons from actual system derivations. They transform cryptic hash changes into actionable information: listing updated packages (with version deltas), highlighting configuration file differences, and revealing dependency shifts. Though their output can be verbose, they provide some information on what’s actually changing so I can then go off and see if there’s any actual breaking changes. Though I rarely find the time to do that and opt for deploy and pray.

Copyright - All Rights Reserved

Comments

To give feedback, send an email to adam [at] this website url.

Donate

If you've found these posts helpful and would like to support this work directly, your contribution would be appreciated and enable me to dedicate more time to creating future posts. Thank you for joining me!

Donate to my blog