{"schemaVersion":1,"id":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments","title":"Nix Flakes as a Docker Replacement for Local Development Environments","description":"A practical guide to Nix Flakes for reproducible dev environments: flake.nix anatomy, devShells, replacing Docker for local development, cross-compilation, and a documented project migration with all the pain points.","language":"en","datePublished":"2025-11-19","dateModified":null,"contentFormat":"text/markdown","content":"DevOps\n\n# Nix Flakes as a Docker Replacement for Local Development Environments\n\n2025-11-198 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/nix-flakes-reproducible-dev-environments>)\n\n## Nix Flakes: Functional Overview\n\n[Nix Flakes](<https://nixos.wiki/wiki/Flakes>) are often described with abstract terminology that obscures their practical value. A more concrete framing follows.\n\nA Nix Flake is a ` flake.nix ` file in your repo that declares, with zero ambiguity:\n\n1. **Exactly** which versions of every tool your project needs\n2. **Exactly** how to build your project\n3. **Exactly** what the development environment looks like\n\nAnd \"exactly\" means hash-pinned-to-a-specific-commit-of-nixpkgs exactly. Not \"Node 20.x\". Not \"whatever Homebrew gives you.\" The specific Node.js build, compiled with specific flags, linked against specific libraries.\n\nHere is the ` flake.nix ` from a representative production service, a Go API with a PostgreSQL dependency and some code generation tools:\n\n```nix\n{\n description = \"Order service API\";\n\n inputs = {\n nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.11\";\n flake-utils.url = \"github:numtide/flake-utils\";\n gomod2nix = {\n url = \"github:nix-community/gomod2nix\";\n inputs.nixpkgs.follows = \"nixpkgs\";\n };\n };\n\n outputs = { self, nixpkgs, flake-utils, gomod2nix }:\n flake-utils.lib.eachDefaultSystem (system:\n let\n pkgs = import nixpkgs {\n inherit system;\n overlays = [ gomod2nix.overlays.default ];\n };\n in {\n devShells.default = pkgs.mkShell {\n buildInputs = with pkgs; [\n go_1_23\n gopls\n gotools\n go-mockery\n sqlc\n postgresql_16\n pgformatter\n protobuf\n protoc-gen-go\n protoc-gen-go-grpc\n buf\n just\n docker-compose\n ];\n\n shellHook = ''\n export GOPATH=\"$PWD/.go\"\n export PATH=\"$GOPATH/bin:$PATH\"\n export PGDATA=\"$PWD/.pgdata\"\n export PGHOST=\"$PWD/.pgdata\"\n echo \"Order service dev shell ready\"\n echo \"Go $(go version | cut -d' ' -f3)\"\n echo \"PostgreSQL $(pg_config --version)\"\n '';\n };\n\n packages.default = pkgs.buildGoApplication {\n pname = \"order-service\";\n version = \"0.1.0\";\n src = ./.;\n modules = ./gomod2nix.toml;\n };\n });\n}\n```\n\nWhen someone runs ` nix develop `, they get a shell with every single tool at the exact version specified. Go 1.23. PostgreSQL 16. protoc, buf, sqlc, all of it. On Linux, on macOS, on Intel, on ARM. The same versions. Every time.\n\n## The Anatomy of flake.nix (The Parts That Matter)\n\nThe following breaks down the structure, since the Nix docs make this harder than necessary:\n\n### inputs\n\n```nix\ninputs = {\n nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.11\";\n flake-utils.url = \"github:numtide/flake-utils\";\n};\n```\n\nThese are your dependencies. ` nixpkgs ` is the package repository (over 100,000 packages). The URL pins to a specific branch. When you run ` nix flake update `, it updates ` flake.lock ` with the exact commit hash, like a ` package-lock.json ` but for your entire system toolchain.\n\n### outputs\n\nThis is where you define what your flake provides. The most common outputs for a development project:\n\n- ` devShells.default `, the development environment (what ` nix develop ` uses)\n- ` packages.default `, the built artifact (what ` nix build ` uses)\n- ` checks `, test suites (what ` nix flake check ` runs)\n\n### devShells (The Money Feature)\n\n```nix\ndevShells.default = pkgs.mkShell {\n buildInputs = [ ... ]; # Tools available in the shell\n shellHook = ''...''; # Runs when entering the shell\n env = { ... }; # Environment variables\n};\n```\n\n` mkShell ` creates an environment, not a container. There's no isolation overhead, no volume mounts, no network bridge. Your editor, your terminal, your git config, they all work normally. You just have different tools on your PATH.\n\n## Why This Beats Docker for Local Dev\n\nPrecision matters here because this is where people get defensive. Docker is excellent for deployment. Nix isn't replacing your production containers. What Nix replaces is the ` Dockerfile.dev ` and ` docker-compose.dev.yml ` that you maintain separately from your production setup.\n\n**Speed**: ` nix develop ` on a warm cache takes 0.3 seconds. Starting a Docker Compose dev environment with 4 services: 12-20 seconds. Over a day of work, that adds up.\n\n**Resource usage**: A Nix devShell uses zero additional RAM beyond the tools themselves. Docker Desktop on macOS reserves a VM with 4-8GB of RAM by default.\n\n**File system performance**: This is the critical factor on macOS. Docker's file system sharing (even with VirtioFS) adds measurable latency to every file operation. For a ` go test ./... ` on a typical service: 4.1s native, 7.8s in Docker. For ` npm install ` on a mid-size project: 12s native, 34s in Docker.\n\n**Debugging**: When something in your Nix shell doesn't work, you can inspect it. ` which go ` shows you the exact Nix store path. ` nix why-depends ` traces the dependency chain. In Docker, you're SSHing into a container and guessing.\n\n### A Real Comparison\n\nMigration checklist, what gets replaced:\n\n| Docker-based | Nix-based |\n| --- | --- |\n| ` Dockerfile.dev ` (47 lines) | ` flake.nix ` (43 lines) |\n| ` docker-compose.dev.yml ` (89 lines) | , (services run natively) |\n| ` .env.docker ` | , (shellHook sets env vars) |\n| ` scripts/setup-dev.sh ` (112 lines) | , |\n| ` scripts/fix-permissions.sh ` | , |\n| ` docs/SETUP.md ` (47 steps) | ` docs/SETUP.md ` (3 steps) |\n\nThe three steps: 1) Install Nix, 2) Run ` nix develop `, 3) Run ` just dev `.\n\n## Cross-Compilation: Nix's Secret Weapon\n\nThis is a genuinely surprising feature. Nix's cross-compilation story is absurdly good:\n\n```nix\npackages = {\n default = pkgs.buildGoApplication {\n pname = \"order-service\";\n version = \"0.1.0\";\n src = ./.;\n modules = ./gomod2nix.toml;\n };\n\n # Cross-compile for ARM64 Linux (for Graviton deployment)\n arm64-linux = pkgs.pkgsCross.aarch64-multiplatform.buildGoApplication {\n pname = \"order-service\";\n version = \"0.1.0\";\n src = ./.;\n modules = ./gomod2nix.toml;\n };\n\n # Build a Docker image without Docker\n dockerImage = pkgs.dockerTools.buildLayeredImage {\n name = \"order-service\";\n tag = \"latest\";\n contents = [ self.packages.${system}.default ];\n config = {\n Cmd = [ \"/bin/order-service\" ];\n ExposedPorts = { \"8080/tcp\" = {}; };\n };\n };\n};\n```\n\nThat ` dockerImage ` output is beautiful. It builds an OCI-compliant container image without needing Docker installed. The resulting image is typically 10-30MB because it only includes exactly what the application needs, no base image bloat, no leftover build tools.\n\nARM64 Linux images can be built on macOS developer machines for testing. No QEMU, no emulation. Nix just cross-compiles.\n\n## Migration: An Honest Account\n\nThis process is not always smooth. Here is a representative timeline of migrating a team of 6:\n\n**Week 1**: Writing the ` flake.nix ` and getting it working on one machine. This takes about 3 days due to fighting the Nix language. The error messages are... not great. ` error: attribute 'go_1_23' missing ` when there was actually a typo in the overlay. Helpful.\n\n**Week 2**: Two other senior devs install Nix and try the flake. Immediate issue: a developer's corporate VPN blocks the Nix binary cache (` cache.nixos.org `). Setting up an internal cache on S3 using Attic takes a full day.\n\n**Week 3**: Full team rollout. Three issues typically arise:\n\n1. One dev on an older Intel Mac had a different system architecture string. Fixed by ensuring ` flake-utils.lib.eachDefaultSystem ` was used (it was).\n2. Pre-commit hooks that assumed globally-installed tools need to run inside ` nix develop `.\n3. VS Code's Go extension couldn't find ` gopls ` until the ` direnv ` integration is added.\n\n**Week 4**: Everyone is on Nix. The Docker dev setup gets deleted. No complaints.\n\n## direnv Integration: The Missing Piece\n\nRaw ` nix develop ` requires manually entering the shell every time a terminal opens. ` direnv ` with ` nix-direnv ` automates this:\n\n```bash\n# .envrc\nuse flake\n```\n\nThat's the entire file. When you ` cd ` into the project directory, direnv automatically loads the Nix devShell. Your editor picks up the tools. Your terminal has the right PATH. It's invisible once configured.\n\nInstall it once:\n\n```bash\n# In your global Nix config or home-manager\nnix profile install nixpkgs#direnv nixpkgs#nix-direnv\n```\n\nAdd to your shell rc:\n\n```bash\neval \"$(direnv hook bash)\" # or zsh\n```\n\n## Multiple devShells for Different Workflows\n\nOne ` flake.nix ` can provide multiple shells:\n\n```nix\ndevShells = {\n default = pkgs.mkShell {\n buildInputs = with pkgs; [ go_1_23 gopls postgresql_16 ];\n };\n\n ci = pkgs.mkShell {\n buildInputs = with pkgs; [ go_1_23 postgresql_16 golangci-lint ];\n };\n\n docs = pkgs.mkShell {\n buildInputs = with pkgs; [ mdbook graphviz plantuml ];\n };\n};\n```\n\n` nix develop ` enters the default shell. ` nix develop .#ci ` enters the CI shell. ` nix develop .#docs ` enters the documentation shell. Each is hermetic and reproducible.\n\n## The Binary Cache: Why Second Builds Are Fast\n\nFirst-time ` nix develop ` can be slow, minutes if packages need building from source. But Nix has a binary cache (` cache.nixos.org `) that provides pre-built binaries for most packages. After your first run, subsequent ` nix develop ` calls are nearly instant because everything is cached in the local Nix store.\n\nFor teams, setting up a shared cache is strongly recommended. Attic on an S3 bucket works well:\n\n```nix\n# In your nix.conf or flake's nixConfig\nextra-substituters = https://your-team-cache.s3.amazonaws.com\nextra-trusted-public-keys = your-team-cache:AAAA...==\n```\n\nCI builds populate the cache. Developer machines consume from it. A full ` nix develop ` with warm cache takes under a second.\n\n## Flake Checks: Your CI's Best Friend\n\n```nix\nchecks.${system} = {\n build = self.packages.${system}.default;\n\n lint = pkgs.runCommand \"lint\" {\n buildInputs = [ pkgs.golangci-lint ];\n } ''\n cd ${self}\n golangci-lint run ./...\n touch $out\n '';\n\n test = pkgs.runCommand \"test\" {\n buildInputs = [ pkgs.go_1_23 pkgs.postgresql_16 ];\n } ''\n cd ${self}\n go test -race -count=1 ./...\n touch $out\n '';\n};\n```\n\n` nix flake check ` runs all checks. In CI, this guarantees the exact same tool versions as local development. No more \"but it works on my machine\", it literally cannot differ.\n\n## Honest Assessment\n\nNix Flakes have real downsides:\n\n- **The learning curve is steep**. The Nix language is weird. The documentation is scattered. Error messages are often unhelpful.\n- **The ecosystem is smaller**. You won't find a Stack Overflow answer for every problem.\n- **macOS support is second-class**. It works, but you'll hit edge cases that Linux users never see.\n- **Nix store disk usage grows**. Run ` nix store gc ` periodically or you'll lose 50GB before you notice.\n\nThe bottom line: the time spent learning Nix pays back within the first month. Every new hire since then is productive on day one. CI becomes deterministic. Builds are reproducible across platforms. And 250 lines of shell scripts that no one understood and everyone was afraid to touch get deleted.\n\nThat's worth a weird programming language.\n\n## Frequently asked questions\n\n### Do I need to learn the Nix language to use Flakes?\n\nUnfortunately, yes, at least the basics. Nix the language is functional and lazily evaluated, which trips people up. But for devShells, you mostly need to understand attribute sets, let bindings, and mkShell. The learning curve is steep but short.\n\n### Can Nix Flakes fully replace Docker for development?\n\nFor dependency management and reproducible environments, absolutely. For cases where you need to replicate a specific Linux kernel version or test network configurations, Docker still has its place. In practice, most teams can drop Docker for local dev.\n\n### How does Nix Flakes affect CI/CD pipeline speed?\n\nFirst builds are slower because Nix downloads and builds more precisely. But subsequent builds are dramatically faster due to the binary cache. CI times can drop from 8 minutes to 2 minutes after the cache warms up.\n\n## Tips\n\n1. **Tip:** Always pin your ` nixpkgs ` input to a specific commit or release branch like ` nixpkgs/nixos-25.11 `, never use ` nixpkgs/nixpkgs-unstable ` for team projects.\n2. **Tip:** Use ` nix develop --impure ` during exploration, but always remove ` --impure ` before committing. Impure shells hide missing dependencies that will bite your CI.\n3. **Tip:** ` nix build .#dockerImage ` creates OCI images without a Dockerfile or Docker daemon, perfect for CI environments that only have Nix.\n4. **Tip:** When a Nix build fails cryptically, ` nix log ` on the failed derivation path gives you the actual build output that ` nix build ` swallows.\n\n- nix\n- flakes\n- devops\n- docker\n- reproducible-builds\n- developer-experience\n- cross-compilation\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Nix Flakes as a Docker Replacement for Local Development Environments","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#title"},{"level":2,"text":"Nix Flakes: Functional Overview","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#nix-flakes-functional-overview"},{"level":2,"text":"The Anatomy of flake.nix (The Parts That Matter)","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#the-anatomy-of-flakenix-the-parts-that-matter"},{"level":3,"text":"inputs","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#inputs"},{"level":3,"text":"outputs","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#outputs"},{"level":3,"text":"devShells (The Money Feature)","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#devshells-the-money-feature"},{"level":2,"text":"Why This Beats Docker for Local Dev","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#why-this-beats-docker-for-local-dev"},{"level":3,"text":"A Real Comparison","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#a-real-comparison"},{"level":2,"text":"Cross-Compilation: Nix's Secret Weapon","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#cross-compilation-nixs-secret-weapon"},{"level":2,"text":"Migration: An Honest Account","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#migration-an-honest-account"},{"level":2,"text":"direnv Integration: The Missing Piece","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#direnv-integration-the-missing-piece"},{"level":2,"text":"Multiple devShells for Different Workflows","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#multiple-devshells-for-different-workflows"},{"level":2,"text":"The Binary Cache: Why Second Builds Are Fast","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#the-binary-cache-why-second-builds-are-fast"},{"level":2,"text":"Flake Checks: Your CI's Best Friend","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#flake-checks-your-cis-best-friend"},{"level":2,"text":"Honest Assessment","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#honest-assessment"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#questions"},{"level":3,"text":"Do I need to learn the Nix language to use Flakes?","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#question-1"},{"level":3,"text":"Can Nix Flakes fully replace Docker for development?","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#question-2"},{"level":3,"text":"How does Nix Flakes affect CI/CD pipeline speed?","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#article-tips"}],"sections":[{"heading":"Nix Flakes as a Docker Replacement for Local Development Environments","level":1,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments","content":"DevOps"},{"heading":"Nix Flakes as a Docker Replacement for Local Development Environments","level":1,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#title","content":"2025-11-198 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/nix-flakes-reproducible-dev-environments>)"},{"heading":"Nix Flakes: Functional Overview","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#nix-flakes-functional-overview","content":"[Nix Flakes](<https://nixos.wiki/wiki/Flakes>) are often described with abstract terminology that obscures their practical value. A more concrete framing follows.\n\nA Nix Flake is a ` flake.nix ` file in your repo that declares, with zero ambiguity:\n\n1. **Exactly** which versions of every tool your project needs\n2. **Exactly** how to build your project\n3. **Exactly** what the development environment looks like\n\nAnd \"exactly\" means hash-pinned-to-a-specific-commit-of-nixpkgs exactly. Not \"Node 20.x\". Not \"whatever Homebrew gives you.\" The specific Node.js build, compiled with specific flags, linked against specific libraries.\n\nHere is the ` flake.nix ` from a representative production service, a Go API with a PostgreSQL dependency and some code generation tools:\n\n```nix\n{\n description = \"Order service API\";\n\n inputs = {\n nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.11\";\n flake-utils.url = \"github:numtide/flake-utils\";\n gomod2nix = {\n url = \"github:nix-community/gomod2nix\";\n inputs.nixpkgs.follows = \"nixpkgs\";\n };\n };\n\n outputs = { self, nixpkgs, flake-utils, gomod2nix }:\n flake-utils.lib.eachDefaultSystem (system:\n let\n pkgs = import nixpkgs {\n inherit system;\n overlays = [ gomod2nix.overlays.default ];\n };\n in {\n devShells.default = pkgs.mkShell {\n buildInputs = with pkgs; [\n go_1_23\n gopls\n gotools\n go-mockery\n sqlc\n postgresql_16\n pgformatter\n protobuf\n protoc-gen-go\n protoc-gen-go-grpc\n buf\n just\n docker-compose\n ];\n\n shellHook = ''\n export GOPATH=\"$PWD/.go\"\n export PATH=\"$GOPATH/bin:$PATH\"\n export PGDATA=\"$PWD/.pgdata\"\n export PGHOST=\"$PWD/.pgdata\"\n echo \"Order service dev shell ready\"\n echo \"Go $(go version | cut -d' ' -f3)\"\n echo \"PostgreSQL $(pg_config --version)\"\n '';\n };\n\n packages.default = pkgs.buildGoApplication {\n pname = \"order-service\";\n version = \"0.1.0\";\n src = ./.;\n modules = ./gomod2nix.toml;\n };\n });\n}\n```\n\nWhen someone runs ` nix develop `, they get a shell with every single tool at the exact version specified. Go 1.23. PostgreSQL 16. protoc, buf, sqlc, all of it. On Linux, on macOS, on Intel, on ARM. The same versions. Every time."},{"heading":"The Anatomy of flake.nix (The Parts That Matter)","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#the-anatomy-of-flakenix-the-parts-that-matter","content":"The following breaks down the structure, since the Nix docs make this harder than necessary:"},{"heading":"inputs","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#inputs","content":"```nix\ninputs = {\n nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.11\";\n flake-utils.url = \"github:numtide/flake-utils\";\n};\n```\n\nThese are your dependencies. ` nixpkgs ` is the package repository (over 100,000 packages). The URL pins to a specific branch. When you run ` nix flake update `, it updates ` flake.lock ` with the exact commit hash, like a ` package-lock.json ` but for your entire system toolchain."},{"heading":"outputs","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#outputs","content":"This is where you define what your flake provides. The most common outputs for a development project:\n\n- ` devShells.default `, the development environment (what ` nix develop ` uses)\n- ` packages.default `, the built artifact (what ` nix build ` uses)\n- ` checks `, test suites (what ` nix flake check ` runs)"},{"heading":"devShells (The Money Feature)","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#devshells-the-money-feature","content":"```nix\ndevShells.default = pkgs.mkShell {\n buildInputs = [ ... ]; # Tools available in the shell\n shellHook = ''...''; # Runs when entering the shell\n env = { ... }; # Environment variables\n};\n```\n\n` mkShell ` creates an environment, not a container. There's no isolation overhead, no volume mounts, no network bridge. Your editor, your terminal, your git config, they all work normally. You just have different tools on your PATH."},{"heading":"Why This Beats Docker for Local Dev","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#why-this-beats-docker-for-local-dev","content":"Precision matters here because this is where people get defensive. Docker is excellent for deployment. Nix isn't replacing your production containers. What Nix replaces is the ` Dockerfile.dev ` and ` docker-compose.dev.yml ` that you maintain separately from your production setup.\n\n**Speed**: ` nix develop ` on a warm cache takes 0.3 seconds. Starting a Docker Compose dev environment with 4 services: 12-20 seconds. Over a day of work, that adds up.\n\n**Resource usage**: A Nix devShell uses zero additional RAM beyond the tools themselves. Docker Desktop on macOS reserves a VM with 4-8GB of RAM by default.\n\n**File system performance**: This is the critical factor on macOS. Docker's file system sharing (even with VirtioFS) adds measurable latency to every file operation. For a ` go test ./... ` on a typical service: 4.1s native, 7.8s in Docker. For ` npm install ` on a mid-size project: 12s native, 34s in Docker.\n\n**Debugging**: When something in your Nix shell doesn't work, you can inspect it. ` which go ` shows you the exact Nix store path. ` nix why-depends ` traces the dependency chain. In Docker, you're SSHing into a container and guessing."},{"heading":"A Real Comparison","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#a-real-comparison","content":"Migration checklist, what gets replaced:\n\n| Docker-based | Nix-based |\n| --- | --- |\n| ` Dockerfile.dev ` (47 lines) | ` flake.nix ` (43 lines) |\n| ` docker-compose.dev.yml ` (89 lines) | , (services run natively) |\n| ` .env.docker ` | , (shellHook sets env vars) |\n| ` scripts/setup-dev.sh ` (112 lines) | , |\n| ` scripts/fix-permissions.sh ` | , |\n| ` docs/SETUP.md ` (47 steps) | ` docs/SETUP.md ` (3 steps) |\n\nThe three steps: 1) Install Nix, 2) Run ` nix develop `, 3) Run ` just dev `."},{"heading":"Cross-Compilation: Nix's Secret Weapon","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#cross-compilation-nixs-secret-weapon","content":"This is a genuinely surprising feature. Nix's cross-compilation story is absurdly good:\n\n```nix\npackages = {\n default = pkgs.buildGoApplication {\n pname = \"order-service\";\n version = \"0.1.0\";\n src = ./.;\n modules = ./gomod2nix.toml;\n };\n\n # Cross-compile for ARM64 Linux (for Graviton deployment)\n arm64-linux = pkgs.pkgsCross.aarch64-multiplatform.buildGoApplication {\n pname = \"order-service\";\n version = \"0.1.0\";\n src = ./.;\n modules = ./gomod2nix.toml;\n };\n\n # Build a Docker image without Docker\n dockerImage = pkgs.dockerTools.buildLayeredImage {\n name = \"order-service\";\n tag = \"latest\";\n contents = [ self.packages.${system}.default ];\n config = {\n Cmd = [ \"/bin/order-service\" ];\n ExposedPorts = { \"8080/tcp\" = {}; };\n };\n };\n};\n```\n\nThat ` dockerImage ` output is beautiful. It builds an OCI-compliant container image without needing Docker installed. The resulting image is typically 10-30MB because it only includes exactly what the application needs, no base image bloat, no leftover build tools.\n\nARM64 Linux images can be built on macOS developer machines for testing. No QEMU, no emulation. Nix just cross-compiles."},{"heading":"Migration: An Honest Account","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#migration-an-honest-account","content":"This process is not always smooth. Here is a representative timeline of migrating a team of 6:\n\n**Week 1**: Writing the ` flake.nix ` and getting it working on one machine. This takes about 3 days due to fighting the Nix language. The error messages are... not great. ` error: attribute 'go_1_23' missing ` when there was actually a typo in the overlay. Helpful.\n\n**Week 2**: Two other senior devs install Nix and try the flake. Immediate issue: a developer's corporate VPN blocks the Nix binary cache (` cache.nixos.org `). Setting up an internal cache on S3 using Attic takes a full day.\n\n**Week 3**: Full team rollout. Three issues typically arise:\n\n1. One dev on an older Intel Mac had a different system architecture string. Fixed by ensuring ` flake-utils.lib.eachDefaultSystem ` was used (it was).\n2. Pre-commit hooks that assumed globally-installed tools need to run inside ` nix develop `.\n3. VS Code's Go extension couldn't find ` gopls ` until the ` direnv ` integration is added.\n\n**Week 4**: Everyone is on Nix. The Docker dev setup gets deleted. No complaints."},{"heading":"direnv Integration: The Missing Piece","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#direnv-integration-the-missing-piece","content":"Raw ` nix develop ` requires manually entering the shell every time a terminal opens. ` direnv ` with ` nix-direnv ` automates this:\n\n```bash\n# .envrc\nuse flake\n```\n\nThat's the entire file. When you ` cd ` into the project directory, direnv automatically loads the Nix devShell. Your editor picks up the tools. Your terminal has the right PATH. It's invisible once configured.\n\nInstall it once:\n\n```bash\n# In your global Nix config or home-manager\nnix profile install nixpkgs#direnv nixpkgs#nix-direnv\n```\n\nAdd to your shell rc:\n\n```bash\neval \"$(direnv hook bash)\" # or zsh\n```"},{"heading":"Multiple devShells for Different Workflows","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#multiple-devshells-for-different-workflows","content":"One ` flake.nix ` can provide multiple shells:\n\n```nix\ndevShells = {\n default = pkgs.mkShell {\n buildInputs = with pkgs; [ go_1_23 gopls postgresql_16 ];\n };\n\n ci = pkgs.mkShell {\n buildInputs = with pkgs; [ go_1_23 postgresql_16 golangci-lint ];\n };\n\n docs = pkgs.mkShell {\n buildInputs = with pkgs; [ mdbook graphviz plantuml ];\n };\n};\n```\n\n` nix develop ` enters the default shell. ` nix develop .#ci ` enters the CI shell. ` nix develop .#docs ` enters the documentation shell. Each is hermetic and reproducible."},{"heading":"The Binary Cache: Why Second Builds Are Fast","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#the-binary-cache-why-second-builds-are-fast","content":"First-time ` nix develop ` can be slow, minutes if packages need building from source. But Nix has a binary cache (` cache.nixos.org `) that provides pre-built binaries for most packages. After your first run, subsequent ` nix develop ` calls are nearly instant because everything is cached in the local Nix store.\n\nFor teams, setting up a shared cache is strongly recommended. Attic on an S3 bucket works well:\n\n```nix\n# In your nix.conf or flake's nixConfig\nextra-substituters = https://your-team-cache.s3.amazonaws.com\nextra-trusted-public-keys = your-team-cache:AAAA...==\n```\n\nCI builds populate the cache. Developer machines consume from it. A full ` nix develop ` with warm cache takes under a second."},{"heading":"Flake Checks: Your CI's Best Friend","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#flake-checks-your-cis-best-friend","content":"```nix\nchecks.${system} = {\n build = self.packages.${system}.default;\n\n lint = pkgs.runCommand \"lint\" {\n buildInputs = [ pkgs.golangci-lint ];\n } ''\n cd ${self}\n golangci-lint run ./...\n touch $out\n '';\n\n test = pkgs.runCommand \"test\" {\n buildInputs = [ pkgs.go_1_23 pkgs.postgresql_16 ];\n } ''\n cd ${self}\n go test -race -count=1 ./...\n touch $out\n '';\n};\n```\n\n` nix flake check ` runs all checks. In CI, this guarantees the exact same tool versions as local development. No more \"but it works on my machine\", it literally cannot differ."},{"heading":"Honest Assessment","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#honest-assessment","content":"Nix Flakes have real downsides:\n\n- **The learning curve is steep**. The Nix language is weird. The documentation is scattered. Error messages are often unhelpful.\n- **The ecosystem is smaller**. You won't find a Stack Overflow answer for every problem.\n- **macOS support is second-class**. It works, but you'll hit edge cases that Linux users never see.\n- **Nix store disk usage grows**. Run ` nix store gc ` periodically or you'll lose 50GB before you notice.\n\nThe bottom line: the time spent learning Nix pays back within the first month. Every new hire since then is productive on day one. CI becomes deterministic. Builds are reproducible across platforms. And 250 lines of shell scripts that no one understood and everyone was afraid to touch get deleted.\n\nThat's worth a weird programming language."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#questions","content":""},{"heading":"Do I need to learn the Nix language to use Flakes?","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#question-1","content":"Unfortunately, yes, at least the basics. Nix the language is functional and lazily evaluated, which trips people up. But for devShells, you mostly need to understand attribute sets, let bindings, and mkShell. The learning curve is steep but short."},{"heading":"Can Nix Flakes fully replace Docker for development?","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#question-2","content":"For dependency management and reproducible environments, absolutely. For cases where you need to replicate a specific Linux kernel version or test network configurations, Docker still has its place. In practice, most teams can drop Docker for local dev."},{"heading":"How does Nix Flakes affect CI/CD pipeline speed?","level":3,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#question-3","content":"First builds are slower because Nix downloads and builds more precisely. But subsequent builds are dramatically faster due to the binary cache. CI times can drop from 8 minutes to 2 minutes after the cache warms up."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#article-tips","content":"1. **Tip:** Always pin your ` nixpkgs ` input to a specific commit or release branch like ` nixpkgs/nixos-25.11 `, never use ` nixpkgs/nixpkgs-unstable ` for team projects.\n2. **Tip:** Use ` nix develop --impure ` during exploration, but always remove ` --impure ` before committing. Impure shells hide missing dependencies that will bite your CI.\n3. **Tip:** ` nix build .#dockerImage ` creates OCI images without a Dockerfile or Docker daemon, perfect for CI environments that only have Nix.\n4. **Tip:** When a Nix build fails cryptically, ` nix log ` on the failed derivation path gives you the actual build output that ` nix build ` swallows.\n\n- nix\n- flakes\n- devops\n- docker\n- reproducible-builds\n- developer-experience\n- cross-compilation\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/nix-flakes-reproducible-dev-environments"},{"text":"Nix Flakes","url":"https://nixos.wiki/wiki/Flakes"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments"},"headline":"Nix Flakes as a Docker Replacement for Local Development Environments","description":"A practical guide to Nix Flakes for reproducible dev environments: flake.nix anatomy, devShells, replacing Docker for local development, cross-compilation, and a documented project migration with all the pain points.","datePublished":"2025-11-19","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"DevOps","keywords":"nix, flakes, devops, docker, reproducible-builds, developer-experience, cross-compilation","wordCount":1524,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/nix-flakes-reproducible-dev-environments"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Nix Flakes as a Docker Replacement for Local Development Environments","item":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Do I need to learn the Nix language to use Flakes?","acceptedAnswer":{"@type":"Answer","text":"Unfortunately, yes, at least the basics. Nix the language is functional and lazily evaluated, which trips people up. But for devShells, you mostly need to understand attribute sets, let bindings, and mkShell. The learning curve is steep but short."}},{"@type":"Question","name":"Can Nix Flakes fully replace Docker for development?","acceptedAnswer":{"@type":"Answer","text":"For dependency management and reproducible environments, absolutely. For cases where you need to replicate a specific Linux kernel version or test network configurations, Docker still has its place. In practice, most teams can drop Docker for local dev."}},{"@type":"Question","name":"How does Nix Flakes affect CI/CD pipeline speed?","acceptedAnswer":{"@type":"Answer","text":"First builds are slower because Nix downloads and builds more precisely. But subsequent builds are dramatically faster due to the binary cache. CI times can drop from 8 minutes to 2 minutes after the cache warms up."}}]}]}],"contentHash":"ca0f3b6e6f775da400f543bb24924b4ab12ad7b68df1c16c5b8cec244c46ed2e","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":70649,"contentBytes":12831}}
