12 min read
Dillon Browne

Patch Software with Nix

Master Nix overlays and patches for production infrastructure. Apply custom fixes, backport security updates, and maintain reproducible builds. Deploy today.

nix devops infrastructure security
Patch Software with Nix

In my years managing infrastructure at scale, I’ve encountered countless scenarios where I needed to patch software before official updates arrived. A zero-day vulnerability requires an immediate fix, or a critical bug blocks a production deployment. Traditional package managers leave you waiting for upstream maintainers or wrestling with brittle build scripts.

Nix changed how I approach software patching entirely. The ability to patch any software declaratively, share those patches across teams, and guarantee bit-for-bit reproducibility transformed our infrastructure reliability.

Why Nix Transforms Software Patching

Nix’s functional package management model treats software builds as pure functions. Each package derivation specifies inputs, build steps, and outputs in a deterministic way. When you need to patch software, you’re not modifying system files—you’re composing a new derivation that extends the original.

This approach delivers several advantages I’ve leveraged in production:

Reproducibility: Every developer and CI system builds the identical binary from the same derivation. Patches apply consistently across environments.

Isolation: Patched packages coexist with original versions. You can run multiple versions simultaneously without conflicts.

Rollbacks: If a patch introduces regressions, rolling back is atomic. The previous generation remains available in the Nix store.

Auditability: Every patch lives in version control. You can trace exactly what changed, when, and why.

Deploy Nix Overlay Patterns

Nix overlays provide the primary mechanism for patching packages. An overlay is a function that takes the original package set and returns a modified version. I’ve developed several patterns that work reliably in production environments.

Apply Security Patches with Overlays

Here’s how I applied an urgent security patch to OpenSSL before the official Nix package updated:

# overlays/openssl-security.nix
final: prev: {
  openssl = prev.openssl.overrideAttrs (oldAttrs: {
    patches = (oldAttrs.patches or []) ++ [
      (prev.fetchpatch {
        name = "CVE-2024-XXXXX.patch";
        url = "https://github.com/openssl/openssl/commit/abc123.patch";
        hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
      })
    ];
  });
}

The overrideAttrs function creates a new derivation based on the original, adding our security patch. The fetchpatch function downloads and verifies the patch using content addressing—ensuring the patch content matches the expected hash.

I incorporate this overlay in flake.nix:

{
  description = "Production infrastructure";
  
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  };
  
  outputs = { self, nixpkgs }: {
    nixosConfigurations.server = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ({ config, pkgs, ... }: {
          nixpkgs.overlays = [ 
            (import ./overlays/openssl-security.nix)
          ];
        })
        ./configuration.nix
      ];
    };
  };
}

Override Build Configuration

Sometimes you need to change build-time configuration rather than apply code patches. I encountered this when we needed Nginx built with additional modules for observability:

# overlays/nginx-custom.nix
final: prev: {
  nginx = prev.nginx.override {
    modules = with prev.nginxModules; [
      moreheaders
      vts
      (prev.nginxModules.rtmp.override {
        ffmpeg = prev.ffmpeg-full;
      })
    ];
  };
}

The override function modifies the input arguments to the package derivation. This approach changes what gets built without touching the actual build recipe.

Manage Local Patch Files

For custom patches that don’t exist upstream, I store them in the repository alongside the overlay:

# overlays/postgresql-performance.nix
final: prev: {
  postgresql_15 = prev.postgresql_15.overrideAttrs (oldAttrs: {
    patches = (oldAttrs.patches or []) ++ [
      ./patches/postgresql-connection-pooling.patch
      ./patches/postgresql-query-cache.patch
    ];
  });
}

I maintain patch files in ./patches/ with detailed headers explaining the purpose:

# patches/postgresql-connection-pooling.patch
From: Dillon Browne <dillon@example.com>
Date: Mon, 15 Jan 2026 14:23:45 -0800
Subject: Add connection pooling timeout configuration

This patch adds a configurable timeout for connection pool exhaustion,
preventing cascade failures during traffic spikes. Applied until
upstream PR #12345 merges.

---
diff --git a/src/backend/utils/pool.c b/src/backend/utils/pool.c
index abc123..def456 100644
--- a/src/backend/utils/pool.c
+++ b/src/backend/utils/pool.c
@@ -42,6 +42,9 @@
+    if (pool_timeout > 0) {
+        wait_for_connection(pool_timeout);
+    }

Version controlling patches with explanatory context proved invaluable during incident reviews and knowledge transfer.

Master Advanced Nix Patching Techniques

Coordinate Multi-Package Patches

Some patches require coordinated changes across dependent packages. When I needed to enable a feature in both PostgreSQL and the corresponding Rust client library:

final: prev: {
  postgresql_15 = prev.postgresql_15.overrideAttrs (oldAttrs: {
    configureFlags = oldAttrs.configureFlags ++ [
      "--enable-custom-protocol"
    ];
    patches = (oldAttrs.patches or []) ++ [
      ./patches/pg-custom-protocol.patch
    ];
  });
  
  # Ensure the Rust library uses our patched PostgreSQL
  sqlx-cli = prev.sqlx-cli.override {
    postgresql = final.postgresql_15;
  };
}

The final and prev parameters in overlays enable this coordination. prev references the unmodified package set, while final references the fully composed result after all overlays apply. Using final.postgresql_15 ensures the Rust tooling links against our patched version.

Enable Conditional Patching

In production, I often need patches that only apply in specific environments. NixOS makes this straightforward:

{ config, pkgs, lib, ... }:
{
  nixpkgs.overlays = lib.optionals config.services.monitoring.enable [
    (final: prev: {
      prometheus = prev.prometheus.overrideAttrs (oldAttrs: {
        patches = (oldAttrs.patches or []) ++ [
          ./patches/prometheus-custom-metrics.patch
        ];
      });
    })
  ];
  
  services.monitoring.enable = true;
}

This pattern applies the Prometheus patch only when monitoring is enabled, keeping development environments lightweight.

Deploy Cross-Platform Patches

Managing infrastructure across x86_64 and ARM64 systems required platform-specific patches. Nix’s stdenv provides the necessary context:

final: prev: {
  myapp = prev.myapp.overrideAttrs (oldAttrs: {
    patches = (oldAttrs.patches or []) ++ 
      lib.optionals prev.stdenv.isAarch64 [
        ./patches/arm64-optimizations.patch
      ] ++
      lib.optionals prev.stdenv.isx86_64 [
        ./patches/x86-simd.patch
      ];
  });
}

Validate Patches Before Production

Patched software requires rigorous testing before production deployment. I’ve developed a testing workflow that catches issues early.

Automate Build-Time Testing

Nix’s passthru.tests attribute enables declarative testing:

final: prev: {
  myservice = prev.myservice.overrideAttrs (oldAttrs: {
    patches = (oldAttrs.patches or []) ++ [
      ./patches/myservice-fix.patch
    ];
    
    passthru.tests = {
      basic = prev.nixosTest {
        name = "myservice-basic";
        nodes = {
          machine = { ... }: {
            services.myservice.enable = true;
          };
        };
        testScript = ''
          machine.wait_for_unit("myservice.service")
          machine.succeed("curl http://localhost:8080/health")
        '';
      };
    };
  });
}

Running nix build .#myservice.tests.basic validates the patch before deployment.

Run Integration Tests

For complex patches affecting multiple services, I use NixOS VM tests:

# tests/patched-stack.nix
import <nixpkgs/nixos/tests/make-test-python.nix> {
  name = "patched-stack-integration";
  
  nodes = {
    database = { ... }: {
      services.postgresql = {
        enable = true;
        package = pkgs.postgresql_15;  # Uses our overlay
      };
    };
    
    app = { ... }: {
      services.myapp = {
        enable = true;
        databaseHost = "database";
      };
    };
  };
  
  testScript = ''
    database.wait_for_unit("postgresql.service")
    app.wait_for_unit("myapp.service")
    
    # Verify the patched feature works
    app.succeed("myapp-cli test-custom-protocol")
  '';
}

These tests run in isolated VMs, ensuring patches don’t introduce regressions.

Deploy Nix Patches to Production

Deploying patched software to production requires careful orchestration. My approach balances safety with velocity.

Configure Staged Rollouts

I use NixOS configurations to define deployment stages:

# flake.nix
{
  nixosConfigurations = {
    # Canary: receives patches first
    canary = nixpkgs.lib.nixosSystem {
      modules = [
        { nixpkgs.overlays = [ patchOverlay ]; }
        ./hosts/canary.nix
      ];
    };
    
    # Production: only after canary validates
    prod = nixpkgs.lib.nixosSystem {
      modules = [
        # Conditionally enable patch
        ({ config, ... }: lib.mkIf config.deployment.enablePatch {
          nixpkgs.overlays = [ patchOverlay ];
        })
        ./hosts/production.nix
      ];
    };
  };
}

Execute Safe Rollbacks

NixOS generations provide instant rollback capability:

# On production servers
nixos-rebuild boot --rollback
reboot

# Or for immediate rollback without reboot
nixos-rebuild switch --rollback

The previous generation remains in the bootloader, allowing recovery even if the new system fails to boot.

Monitor Post-Deployment Health

After deploying patches, I monitor key metrics:

# scripts/validate-patch.py
import requests
import time

def validate_deployment(host, expected_version):
    """Verify patched software runs correctly."""
    for attempt in range(30):
        try:
            resp = requests.get(f"http://{host}/version")
            if resp.json()['version'] == expected_version:
                print(f"✓ {host} running patched version")
                return True
        except requests.RequestException:
            pass
        time.sleep(2)
    
    print(f"✗ {host} failed to deploy patched version")
    return False

# Validate canary first
if validate_deployment("canary.example.com", "1.2.3-patched"):
    # Proceed to production
    validate_deployment("prod-01.example.com", "1.2.3-patched")

Lessons from Production

After managing Nix-based infrastructure for several years, I’ve learned important lessons about patching strategies.

Start with upstream: Before creating patches, check if the fix exists upstream or in nixpkgs. Many issues have existing solutions.

Document extensively: Future you will appreciate detailed patch headers and commit messages explaining the rationale.

Automate testing: Manual testing doesn’t scale. Invest in NixOS VM tests early.

Plan for patch removal: Every patch is technical debt. Schedule reviews to remove patches once upstream merges fixes.

Monitor patch complexity: If a patch grows beyond 100 lines, consider forking the package instead of maintaining a complex patch.

Nix transforms software patching from a risky manual process into a reproducible, testable operation. The declarative approach enables teams to collaborate on patches through version control while maintaining deployment predictability across all environments.

When the next zero-day vulnerability emerges, Nix-based infrastructure gives you confidence in your patching process—with the ability to roll back instantly if needed. That operational safety, combined with reproducible builds and comprehensive testing, makes the learning curve worthwhile.

Start small: patch a single package in development, validate with automated tests, then expand to production. Your future self will thank you when that critical security update arrives.

Found this helpful? Share it with others: