Build Stack Review

declarative environment managers for teams spanning multiple operating systems

Correspondent · · 12 min read
Cover illustration for “declarative environment managers for teams spanning multiple operating systems”
Reproducible Development Environments · August 22, 2026 · 12 min read · 2,660 words

A team spanning ARM MacBooks, x86-64 Linux CI runners, and a Windows contractor's laptop is fighting a dependency graph problem wearing an OS costume, and confusing the two is what makes "works on my machine" so expensive to chase down. Declarative environment managers, especially the Nix-derived generation of them, fix this by making the resolved set of dependencies, not the machine that resolved them, the thing everyone can check against. This kind of drift is exactly the failure mode that makes works on my machine so costly to diagnose.

The everyday version of this is almost boring in how familiar it is. A developer on Apple Silicon runs npm install and gets one dependency tree; a CI runner on x86-64 Linux runs the same command against the same package.json a day later and gets a slightly different one, because some transitive dependency published a patch release overnight and nothing pinned it. A Windows contributor hits a native module that never compiled cleanly outside WSL2 in the first place. Nothing about this is exotic. It's just Tuesday.

What "declarative" actually means for an environment, and what it does not

Venn diagram: Declarative vs. Imperative Environments. Compares Declarative and Imperative; overlap: Shared Traits.

Declarative versus imperative comes down to one question: does your spec describe an end state, or a sequence of commands? A README that says "run apt-get install, then brew install, then pip install, in that order" is imperative. It depends on the order you ran things, the OS underneath, and usually some tribal knowledge about which step to skip on a Mac. A declarative spec names every dependency, ideally down to a content hash, so the tool derives the same closure of packages no matter what machine is doing the deriving.

That's the structural basis for how Nix and its descendants behave. When a spec is purely functional, meaning every output is a deterministic function of its inputs, and those inputs are content-addressed, running it on macOS and running it on Linux produce semantically equivalent environments, even though the compiled binaries differ by architecture. Change one input, even a patch version three layers down, and the output path changes with it. Drift stops hiding.

It's worth being precise about what declarative actually requires, because it's less than people assume. A single operating system, a container runtime, a shared programming language: these are implementation choices some tools make, not properties baked into the definition itself. The real test is simpler than any of that: can someone on a different OS than yours clone the repo and reproduce your working environment from the spec alone, without opening a README full of "if you're on Mac, do this instead" branches? A setup that fails that test is documentation with a declarative-sounding name attached.

Researchers working on reproducible builds have started calling this "Development Environment as Code," echoing Infrastructure as Code on purpose. The environment becomes a version-controlled, diffable, reviewable artifact, the same way a Terraform file describes a server instead of a runbook describing how someone once configured one by hand.

How Nix's content-addressed store makes cross-OS reproducibility structurally possible

Nix's core mechanism matters even if you never write a line of it, because every tool below sits on top of this. A Nix derivation declares every input to a build: source code, patches, every dependency, the build script itself. The output lives at a path in an immutable store, derived from a hash of those inputs. Change any input, even a compiler flag, and you get a new path. Nothing overwrites anything else.

Here's a consequence that's easy to undervalue until you've lived without it: multiple versions of the same package coexist on one machine with zero collision. Two projects needing different Python versions don't fight over /usr/local/bin/python. Each gets its own store path, full stop.

For a cross-OS team, the same declarative spec produces native binaries on ARM macOS, compiled with Clang against Apple's system frameworks, and native binaries on x86-64 Linux, compiled with GCC against glibc. There's no emulation, no Rosetta tax. And because old store paths don't disappear just because a new one exists, rollback is atomic: break something with an update, and the previous environment is still sitting there, ready to switch back to.

There's a supply chain angle too, and it's a bigger deal than it looks at first glance. Every package in a Nix closure traces back to a content-addressed derivation, so the environment itself functions as a structural bill of materials. Provenance isn't a report someone runs after the fact; it's baked into how the thing got built in the first place.

Getting there does cost something, though. The Nix language has a real learning curve, and raw Nix is not where most teams should start. macOS support is solid but trails Linux in a few corners; Windows support runs through WSL2, not natively. These are the honest costs of the foundation the next layer of tools exists to paper over.

The tool landscape: where each option sits on the tradeoff between power and approachability

Every tool here chases the same goal: reproducible, cross-OS environments. Where they differ is how much of the underlying Nix model they expose versus hide, and that's the axis worth judging them on, not a feature checklist.

mise (formerly rtx) sits at the approachable end. It solves the language-runtime layer, pinning Node, Python, or Ruby versions consistently across machines. It's fast, the config reads cleanly, and teams pick it up in an afternoon. What it doesn't touch is OS-level system libraries or transitive C dependencies, so treat it as the right entry point when the pain is "everyone's on a different Node version," not a destination for polyglot stacks with native extensions.

Dev Containers, the spec Microsoft maintains, take a different route entirely: define the environment inside a Docker container via a .devcontainer/devcontainer.json file, and tools like VS Code or GitHub Codespaces detect it and drop you straight in. This kills cross-OS drift by making the OS irrelevant, since everyone runs the same container regardless of laptop. But the drift risk just relocates, up to the container image itself. Floating base image tags mean your container can change on the next pull with nobody touching the Dockerfile; digest-pinning fixes that, but it takes a discipline most teams don't actually sustain past the first few months. It also needs a container runtime, which rules it out anywhere Docker isn't welcome.

Devbox, from Jetify, wraps Nix's reproducibility behind a CLI that reads more like Homebrew than a functional language. You write devbox add nodejs@20, not a derivation by hand. Check in the config and the generated lockfile, and you get the same closure guarantees raw Nix offers, minus the syntax. It runs in CI off a single action reference, so the CI environment and the laptop environment pull from one source instead of two configs merely hoping to agree with each other.

devenv, from Cachix, sits a step further toward the power end. The config is a devenv.nix file, actual Nix, which leaks more of the abstraction than Devbox's JSON but hands you direct access to the underlying model when you need custom derivations or deep overrides. One project-centric spec replaces the usual sprawl of Dockerfiles, Brewfiles, and pip requirement files; a contributor on ARM macOS and one on x86-64 NixOS both walk away with native binaries from that same file.

Flox targets a different scale of problem entirely, platform teams publishing and governing versioned environments across a whole organization, not one repo. Environment definitions come as reviewable diffs rather than opaque VM snapshots or container images, so updates propagate as targeted, auditable changes instead of "everyone rebuild from scratch." It handles composition too, letting a platform team publish a base environment that individual developers extend without breaking the org's dependency contract. And because it's Nix underneath, provenance and SBOM data come along for free instead of needing a separate audit pass. It runs natively on macOS and Linux, ARM and x86, no virtualization required.

No tool on this list wins outright. The question worth asking is where your actual pain sits on that power-to-approachability line, not which option wins a comparison chart.

Table: Declarative Environment Tools Compared. Compares Best For, Abstraction Level, Cross-OS Approach, Key Limitation, and 1 more by mise, Dev Containers, Devbox, devenv, and 1 more.

How a cross-OS team migrates from ad hoc setup to a declarative baseline without stopping work

Migrations that jump straight to the most powerful tool tend to stall, because the team hasn't felt enough pain yet to justify the learning curve. A graduated approach works better in practice.

Start by pinning language versions with mise, paired with a minimal, honest contract in the README (something like mise install && uv sync, plus a compose file for anything stateful like a database). This step alone kills off a surprising share of "works on my machine" tickets, since it removes the most common source of drift: mismatched Node or Python versions across machines.

The second stage tends to arrive on its own, a few months in, once the environment outgrows what version pinning can manage. System libraries creep in. A native dependency needs a specific compiler. mise's ceiling becomes visible fast at that point. That's the moment to move to Devbox or devenv, check the lockfile into version control, and, this part actually matters, point CI at the exact same config instead of maintaining a parallel one that drifts on its own schedule.

The third stage is organizational, not technical. Once multiple teams need governed, versioned base environments, Flox or a raw Nix overlay earns its complexity, managing composition and provenance at a scale a single project-level config was never built to handle.

One thing decides whether any of these three stages actually worked: local and CI have to run off the same spec, not a similar one. "Passes locally, fails in CI" isn't a bug to chase case by case; it's a signal you've got two environment definitions pretending to be one. Platform teams should treat environment updates the way they treat code changes: diffed, reviewed, tested, merged, rather than announced in a Slack message and hoped for.

A useful forcing function here is the onboarding test. If a new contributor on an OS nobody else uses can't get a working environment from the repo alone in under a day, the environment isn't declarative in any way that matters, whatever the config files claim.

What the dependency graph as source of truth changes about supply chain security

The timing isn't a coincidence. Software supply chain attacks have escalated sharply over the past several years, and attackers have climbed the stack, targeting CI/CD pipelines and build tooling directly instead of application code. Compromising one widely used build step reaches far more victims than compromising one app ever could. One pattern that's shown up repeatedly: attackers modifying historical tags on widely used CI actions, retroactively changing what a pinned-looking reference actually points to, exposing downstream repos that had assumed a tag was immutable. The attack surface now includes the environment definition layer itself, not just the code running inside it.

Regulation has caught up, more or less. Executive Order 14028 in the US and the EU's Cyber Resilience Act have both pushed software bills of materials from nice-to-have into something closer to a compliance expectation in a lot of contexts. The harder question is whether your team can act on an SBOM once it exists. An inventory nobody can query fast enough when a CVE drops doesn't reduce risk; it just satisfies an audit checkbox.

This is where content-addressed environments earn their keep. In a Nix-derived environment, every package in the closure traces back to a hash-verified derivation, so the SBOM isn't a document some scanning tool bolts on after the fact; it's implicit in how the environment got built. When a CVE drops for some library, you can query exactly which environments, across every historical version of your spec, include the affected package, without a manual crawl through lockfiles.

The definition of "supply chain" is stretching in real time too. Teams now ship AI model weights and MCP servers alongside traditional code dependencies, and the same provenance logic applies to both. Content-addressed storage and declarative specs matter just as much for an ML bill of materials as for a conventional SBOM. The sensible move is building SBOM generation and vulnerability scanning directly into the same CI pipeline that consumes the declarative environment spec, rather than running compliance as a separate workflow that checks in weeks late.

Where declarative environments fit inside a platform engineering strategy

Platform teams live with a genuine tension. Mandate too much tooling uniformity and you get bottlenecks and resentment; leave developers to choose freely and you get exactly the drift problem this piece started with. Declarative environments offer a real middle path: platform teams publish base environments with known, verifiable provenance, and developers extend those bases within guardrails, instead of starting from nothing or getting handed something rigid.

Inside an internal developer platform, this changes what environment management looks like day to day. Environment definitions become first-class artifacts, versioned, discoverable, wired into self-service onboarding instead of buried in a wiki page last touched two years ago. A new hire runs one command and gets the canonical environment for their team's project. No support ticket, no archaeology through stale setup docs. When a platform team updates a base environment, that update propagates as a reviewable diff teams can inspect and adopt on their own schedule, rather than a breaking change announced in an all-hands.

The onboarding math backs this up. Organizations that connect declarative environments to IDP-driven onboarding report meaningfully shorter ramp times and far fewer setup-related tickets in a new hire's first weeks. That's not a small efficiency gain. Turnover runs disproportionately high in the earliest weeks of a job, and a broken or frustrating Day One environment is a well-documented contributor to that early attrition. Environment friction is a retention problem, more than most people give it credit for.

One more shift platform teams can't sleep on: AI coding agents increasingly run alongside human developers in CI and locally, and they need the same environmental discipline humans do. An agent that resolves its own dependencies outside the declared spec, quietly pip-installing whatever it decides it needs mid-task, reintroduces drift through a door most teams haven't thought to lock yet. MCP servers and model weights deserve the same content-addressing and provenance tracking as any other dependency, with no exemption because they're new and shiny.

The platform team's job is shifting because of all this: away from provisioning environments one ticket at a time, toward publishing and maintaining the declarative specs that let every team provision its own.

Choosing the right tool given your team's actual starting point

None of this argues for the most powerful tool on the shelf. It argues for matching the tool to the pain you actually have, not the pain described on a vendor's landing page.

If your biggest recurring headache is "everyone's on a different Node version," mise solves that in an afternoon; don't reach past it yet. If you're already committed to Docker and VS Code, Dev Containers give you cross-OS consistency with a tool most developers already have open, provided someone actually owns digest-pinning discipline instead of letting base images float. If you want Nix's reproducibility guarantees without asking your team to learn a new language, Devbox is built for exactly that trade. If you need deeper control, custom derivations, or overrides a JSON config can't express, devenv's leakier abstraction is worth the extra complexity. And if you're a platform team governing environments across a dozen repos and several teams, Flox is built for that scale specifically, not as a fancier version of the others.

The operating system was never a reliable source of truth, and trying to force it to be one just trades drift for a different kind of friction. The dependency graph, made explicit, content-addressed, and checked into version control, can actually serve that role. Which tool you pick just determines how much of that machinery you see, and how much runs quietly underneath, out of sight, doing its job.

Sources

  1. flox.dev
  2. flox.dev
  3. blog.brightcoding.dev
  4. internaldeveloperplatform.org

More in Reproducible Development Environments