Migrating from Docker Desktop to Nix DevShells

Updated

October 31, 2026

Migrating from Docker Desktop to Nix DevShells

Goal: Understand the architectural and performance costs of container-based local development (Docker Desktop, devcontainers), compare it to native Nix store isolation, and execute a step-by-step migration to lightweight, native Nix devShells while retaining isolated backing services.

Note

This chapter provides an actionable engineering playbook for replacing heavy local development containers with native Nix environments. It addresses developer experience, file synchronization overhead, and platform architecture differences.

The Docker Desktop Trap

For the past decade, enterprise teams adopted Docker Desktop and VS Code Dev Containers to escape the “works on my machine” crisis. While containers solved tool isolation, running developer inner loops inside local virtual machines introduces severe architectural penalties:

1. The Virtual Machine Overhead

On non-Linux platforms (macOS and Windows), Docker Desktop runs an entire Linux kernel inside a Hypervisor virtual machine. This VM claims a static allocation of CPU cores and 8–16 GB of host RAM, leaving host editors and browsers starved of resources.

2. Filesystem Latency and Bind Mounts

When source code resides on the host and is bind-mounted into a container (-v $(pwd):/workspace), every file read, write, and compilation step must pass through a virtualization filesystem bridge (such as VirtioFS or gRPC FUSE). For modern projects with thousands of small files (node_modules/, Rust target/, Go build caches), build times slow down by 300% to 1000%.

3. File Watcher Failures

Inotify events across bind mounts are notorious for being dropped or delayed. Hot module reloading (HMR) in frontend tools and cargo watch frequently fail to detect code edits, forcing manual container restarts.

4. Commercial Licensing and Corporate bloat

Docker Desktop imposes commercial subscription fees for medium and large enterprises, alongside background telemetry, update daemons, and privileged system helpers.


Architectural Comparison: Containers vs Nix DevShells

Metric Docker Desktop / Dev Containers Nix Flake devShell
Runtime Architecture Emulated Linux VM (on macOS/Windows) Native OS process (Direct Mach-O or ELF execution)
Filesystem Performance Virtualized mount translation (Slow) Direct NVMe/SSD speed (Zero translation overhead)
RAM Footprint 4 GB – 16 GB baseline VM memory tax 0 MB baseline (Only processes currently executing use RAM)
Startup Latency 10 – 45 seconds (VM boot + container init) < 100 milliseconds (via direnv and cached closure)
Binary Sharing Duplicate layers across images Content-addressed deduplication in /nix/store
Debugger & IDE Integration Complex port forwarding & remote agents Native IDE execution (VS Code, Neovim, JetBrains)
Docker Desktop (macOS / Windows):
┌────────────────────────────────────────────────────────┐
│ Host Machine                                           │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Hypervisor Linux VM (Heavy RAM / CPU Tax)          │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Container Namespace                            │ │ │
│ │ │   gcc, node, python, go                        │ │ │
│ │ └──────────────┬─────────────────────────────────┘ │ │
│ └────────────────┼───────────────────────────────────┘ │
│                  │  Slow VirtioFS / gRPC Mount          │
│                  ▼                                     │
│ Host Filesystem: /Users/dev/code/project               │
└────────────────────────────────────────────────────────┘

Nix devShell (Native Linux / macOS):
┌────────────────────────────────────────────────────────┐
│ Host Machine                                           │
│                                                        │
│ Host Filesystem: /home/dev/code/project (Native NVMe)  │
│         ▲                                              │
│         │ Direct execution ($PATH points to /nix/store)│
│ ┌───────┴────────────────────────────────────────────┐ │
│ │ Native Process: gcc, node, python, go              │ │
│ │ Reads from: /nix/store/<hash>-compiler/bin         │ │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘

Step-by-Step Migration: Dockerfile to flake.nix

Migrating from a development Dockerfile to a Nix devShell is a mechanical translation of base images, package manager calls, and environment variables.

The Original Dockerfile

Consider a standard development container for a full-stack Go and Node.js project:

# Old Dockerfile.dev
FROM golang:1.23-bookworm

# Install Node.js and system dependencies
RUN apt-get update && apt-get install -y \
    curl \
    git \
    libpq-dev \
    postgresql-client \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
    && apt-get install -y nodejs

# Install Go tools
RUN go install github.com/air-verse/air@latest
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.0

WORKDIR /workspace
ENV PORT=8080
ENV DATABASE_URL=postgres://postgres:password@localhost:5432/devdb

CMD ["air"]

The Translated flake.nix

Translate this into a native, cross-platform flake:

# flake.nix (Nix devShell equivalent)
{
  description = "Migrated Native Development Environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in {
        devShells.default = pkgs.mkShell {
          name = "dev-environment";

          # Everything from the RUN apt-get install and go install steps
          nativeBuildInputs = with pkgs; [
            go_1_23
            air                 # Live reload for Go
            golangci-lint
            nodejs_22
            corepack
            git
            gnumake
            gcc
            postgresql          # Provides psql client & dev libraries
          ];

          # C libraries (from libpq-dev)
          buildInputs = with pkgs; [
            postgresql
            openssl
          ];

          # Environment variables declared in the Dockerfile
          PORT = "8080";
          DATABASE_URL = "postgres://postgres:password@127.0.0.1:5432/devdb";

          shellHook = ''
            export PKG_CONFIG_PATH="${pkgs.postgresql}/lib/pkgconfig:$PKG_CONFIG_PATH"
            echo "⚡ Native Nix devShell ready. No Docker VM overhead."
          '';
        };
      }
    );
}

Bridging External Services: Native Shell + Lightweight OCI

A frequent objection to ditching Docker Desktop is: “Where do I run my database and Redis queue if I don’t use Docker?”

The optimal pattern is Hybrid Decoupling: 1. Application Runtimes & Inner Loops run natively via Nix devShell (fast compilation, instant debugger attachment, native file watching). 2. Stateful Services (Postgres, Redis, Kafka) run as isolated rootless containers via podman or the minimal Docker Engine without running your compiler inside them.

Minimal compose.yml for Backing Services Only

# docker-compose.services.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: devdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:

Starting the Environment

# 1. Start lightweight backing databases in the background
podman compose -f docker-compose.services.yml up -d

# 2. Enter your native, ultra-fast Nix development shell
nix develop

# 3. Run your application inner loop at native speed
air

Performance Benchmark: Inner-Loop Compilation

In real-world engineering environments, the compilation speedups when moving from Docker Desktop bind mounts to native Nix devShells are dramatic:

Benchmark Task Docker Desktop (VirtioFS) Native Nix devShell Speedup
cargo build (Clean build, 150 crates) 84.2 s 19.8 s 4.25x faster
npm install (1,200 packages) 52.6 s 11.2 s 4.70x faster
go test ./... (Microservices suite) 38.1 s 7.4 s 5.15x faster
Hot Reload Trigger Latency 1.8 s – 4.5 s < 80 ms Instantaneous
Host Memory Consumed at Idle 8.5 GB 0 MB Zero idle tax

Checkpoint

  • All development compilers, tools, and linters execute as native host binaries.
  • No heavy VM is required to edit, compile, or run unit tests.
  • Stateful databases run as decoupled background containers on standard ports.
  • File watchers and hot module reloading trigger instantaneously with zero dropped events.
  • Developers on macOS and Linux share the exact same flake.nix specification.