Containers are not whole machines
Docker, WSL2, and NixOS can all improve reproducibility, but they operate at different layers. Confusing those layers is how a development environment becomes surprising.
A container packages a process and its filesystem view. It does not remove the host kernel, filesystem performance, networking path, DNS behavior, clock, resource limits, or orchestration policy. Those still belong to the environment around the container.
That is why the same image can behave differently on Linux, Docker Desktop on Windows, or a NixOS host. The image may be identical, but the runtime boundary is not.
Docker Desktop on WSL2
Docker Desktop's WSL2 backend gives Windows users a Linux kernel boundary that is much closer to ordinary Linux container behavior than the older Hyper-V path. For many development workflows, that is good enough and much more convenient than running a separate Linux machine.
The convenience has a price. File sharing between Windows paths and Linux paths can affect I/O-heavy workloads. Networking crosses a virtualization boundary. Resource settings live in Docker Desktop and WSL configuration, not inside the project repository.
So Docker Desktop can make the application environment repeatable while leaving the developer-machine environment partly external. That distinction matters when a team says "it works in Docker" but one machine still has slow tests or strange DNS behavior.
NixOS as host contract
NixOS attacks a different problem. It makes the host system itself declarative: packages, services, users, and configuration can be described as part of a reproducible operating-system build.
That can make container hosts more predictable. The Docker daemon version, system services, firewall rules, mounted volumes, and supporting tools can be pinned or at least expressed in code. The host stops being a hand-built background assumption.
But NixOS does not make containers unnecessary. A NixOS host and a Docker image solve adjacent problems. One describes the machine. The other describes an application runtime unit. They can complement each other when the boundary is intentional.
Where reproducibility stops
The uncomfortable part is that reproducibility stops at the interfaces. Data volumes, secrets, clocks, network services, CPU architecture, kernel behavior, and external APIs still affect runtime behavior. A deterministic image cannot make production data small, a third-party API reliable, or a laptop filesystem fast.
This is not a reason to reject containers or Nix. It is a reason to be precise. Container reproducibility is not production equivalence. Host reproducibility is not application correctness. Declarative infrastructure is not a substitute for observability.
The better question is always: which layer are we trying to make boring?
A practical split
For a development team, Docker is usually enough to standardize databases, queues, service dependencies, and application commands. WSL2 is often a pragmatic way to make that usable on Windows. NixOS becomes attractive when the host itself is part of the product, the team needs repeatable servers, or the project has many system-level dependencies.
The clean operating rule is to keep each layer honest. Put application dependencies in the image. Put host services in host configuration. Keep secrets outside both. Document the performance-sensitive filesystem paths. Test the production-like path separately from the convenient local path.
That framing avoids the false argument over which tool is more reproducible. They are reproducible at different boundaries.
Boundary examples
Docker, WSL2, and NixOS solve different boundary problems. Docker packages a process and filesystem shape. WSL2 provides a Linux environment on a Windows host. NixOS makes the operating-system configuration reproducible.
FROM ruby:3.3-slim
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
CMD ["bundle", "exec", "puma"]
{ pkgs, ... }:
{
services.postgresql.enable = true;
services.nginx.enable = true;
environment.systemPackages = [ pkgs.ruby pkgs.git ];
}
The Dockerfile describes one app container. The NixOS module describes host services and packages. WSL2 may be where the developer runs both during local work. Treating them as interchangeable leads to confused expectations about networking, persistence, startup, and who owns the kernel boundary.
Questions to ask
The fastest way to choose between these tools is to ask which boundary is actually causing pain. If the problem is packaging one process with its dependencies, Docker is the natural fit. If the problem is getting a Linux development environment on a Windows laptop, WSL2 is the fit. If the problem is making a whole machine reproducible from configuration, NixOS is the fit.
Mixing them can be useful, but the ownership lines should stay visible. A container should not be expected to manage the host like NixOS. WSL2 should not be treated as identical to production Linux. NixOS should not be introduced merely because one application needed a repeatable dependency set.
A useful write-up should therefore name the layer before giving advice. Is the recommendation about the developer shell, the application image, the host service graph, or the production runtime? Once that layer is named, the tradeoffs become much less mysterious.
That separation also makes documentation clearer: local setup notes, container runtime notes, and host configuration notes can live in different files instead of becoming one overloaded recipe.
Sources
- Docker documentation, Docker Desktop WSL2 backend. https://docs.docker.com/desktop/features/wsl/
- Docker documentation, container overview. https://docs.docker.com/get-started/docker-overview/
- NixOS manual. https://nixos.org/manual/nixos/stable/