Nix Development Guide

Why Nix? Eliminates "works on my machine" by pinning every dependency - from Go version to system libraries - in a single file. Build today, rebuild in 5 years, get identical binaries.

When to use:

When not to use:

Quick Start

Prerequisites

Development Workflow

# Enter development shell (installs all dependencies)
nix develop

# Pre-commit hooks are automatically installed in Nix shell
# They include: nixpkgs-fmt for Nix formatting

# Build QNTX binary
nix build

# Build CI container image (defaults to your architecture)
nix build .#ci-image

# Build specific architecture
nix build .#ci-image-amd64
nix build .#ci-image-arm64

# Run checks (flake validation, build verification)
nix flake check

Local Git Hooks

For local development outside the Nix shell, install local Git hooks:

git config core.hooksPath .githooks

This enables:

See .githooks/README.md for details.

How CI Builds Work

Why reproducible builds? Proves the binary you download matches what CI built. Rebuild the same commit later → identical SHA256 hash. No hidden changes.

How it works:

  1. Tag a version → triggers .github/workflows/nix-image.yml
  2. Builds amd64 + arm64 in parallel (Nix handles cross-compilation)
  3. Rebuilds each architecture twice, verifies hashes match (catches non-determinism)
  4. Pushes to Cachix (binary cache) so future builds are instant
  5. Creates Docker multi-arch manifest (one tag works on all platforms)

Why Cachix? First build takes ~30 min (compiles everything). Cachix caches binaries. Next build: ~5 min (just downloads from cache).

Caching strategy:

Common Tasks

Updating Go Dependencies

Why vendorHash? Nix downloads your Go modules during build. Hash proves you got what you expected (security). Wrong hash = build fails.

How to update:

# After changing go.mod/go.sum, run this:
./.githooks/update-nix-hash.sh

# Or manually: let it fail, copy new hash from error
nix build .#qntx  # Fails with "got: sha256-ABC..."
# Copy "got" hash to vendorHash in flake.nix

# Verify
nix build .#qntx

# Commit together
git add flake.nix go.mod go.sum

Upgrading Nixpkgs

Why upgrade? Get newer Go/Rust versions, security patches, bug fixes in build tools.

When to upgrade? Monthly, or when you need a specific package version.

nix flake update nixpkgs  # Updates flake.lock
nix build .#qntx          # Test build still works
nix flake check           # Verify all packages build

git add flake.lock
git commit -m "Update nixpkgs"

Warning: This invalidates Cachix cache (new packages = rebuild everything once).

Adding System Dependencies to Docker Image

When needed: CI tests require a new CLI tool (e.g., jq for JSON processing).

How to add:

  1. Edit flake.nix → find mkCiImage function → add to contents = [...]
  2. If binary needs to be in PATH, add to config.Env PATH list
  3. Test: nix build .#ci-image && docker load < result

Example: Adding jq:

contents = [
  qntx
  pkgs.jq  # Add this
  # ...
];

config.Env = [
  "PATH=${pkgs.lib.makeBinPath [ qntx pkgs.jq ... ]}"  # Add to PATH
];

Nix vs Local Development

Nix Shell

Pros:

Cons:

When to use:

Local Git Hooks

Pros:

Cons:

When to use:

Setup:

git config core.hooksPath .githooks

Troubleshooting

"Hash mismatch for vendor derivation"

Why: Nix hashes your Go modules to detect tampering. You changed go.mod but didn't update the hash → security check fails.

Fix: ./.githooks/update-nix-hash.sh or copy "got:" hash from error to flake.nix.

"Network access not allowed"

Why: Nix sandbox blocks network to force reproducibility. Can't download during build → must declare all deps upfront.

Fix: Update vendorHash (Go deps) or add to contents = [...] (system deps).

CI build fails, local succeeds

Why: You're on different nixpkgs version. CI uses flake.lock, you might have uncommitted flake.lock.

Fix: git add flake.lock and commit it. Or nix flake update to match CI.

Workflow Recommendations

Daily development:

Before pushing:

Monthly maintenance:

After tagging:

Resources

Related Documentation