Skip to main content

Conda in the Packaging Spectrum: From pip to Docker to Nix

· 8 min read
Daniel Bast
Open Source Contributor
Jannis Leidel
Steering council member
Banner image for Conda in the Packaging Spectrum: From pip to Docker to Nix blog post

This is Part 2 of our series "Conda Is Not PyPI: Understanding Conda as a User-Space Distribution".

In Part 1, we explained why conda is not just another Python package manager. Conda packages are distribution units, not libraries. Environments are essentially mini distributions in user-space.


“If conda is a distribution, where does it fit alongside other packaging systems like pip, Docker, and Nix?”

This article places conda on the packaging spectrum, explains the concept of a distribution, and shows how conda’s design strikes a unique balance between flexibility, reproducibility, and accessibility.


What makes something a “distribution”?

A distribution is more than a registry of libraries. It is a curated, versioned, and consistent collection of software packages designed to work together. A true distribution provides:

  1. Completeness at build and runtime. Not just libraries, but compilers, headers, linkers, and runtime dependencies.
  2. Policy and provenance. Standardized build flags, ABI compatibility guarantees, and source traceability.
  3. A structured package format. Metadata that allows a solver to resolve complex dependencies.
  4. Repackaging. Sources from many upstreams (PyPI, npm, GNU Savannah, GitHub, SourceForge, etc.) are rebuilt and normalized into the distro’s format.

This is what Debian, Fedora, and other OS distributions do and it’s also what conda does in user-space.


The libc boundary

Here's a critical difference in how these systems treat the platform C runtime:

  • Conda, depending on the operating system:

    • Linux: Ships nearly the entire user‑space stack (interpreters, libraries, toolchains) but intentionally reuses the host's glibc for forward compatibility.

    • Windows: Packages the MSVC runtime (vs2015_runtime, etc.).

    • macOS: Relies on system libSystem and frameworks.

  • Containers (Docker/Podman): Typical base images bundle a userspace (including glibc or musl). Minimal images can start from scratch, but most workflows inherit a libc from the base layer.

  • Nix: Provides its own glibc in the Linux /nix/store; on macOS it integrates with the platform libc while still isolating other dependencies.

  • pip/npm: Distribute language-scoped artifacts only; assume system toolchains and the C runtime are already present.

Conda packages' approach keeps environments lighter while ensuring compatibility via conservative build baselines and virtual packages that encode host facts.

To ensure portability, conda packages are built against the oldest supported libc/OS runtime baselines, making them forward compatible across a wide range of newer systems.

Other user-space package managers, like spack and homebrew, also make their own decisions on packaging glibc depending on the platform, etc. And thus incur similar tradeoffs with respect to environment sizes.

Definition

Forward compatibility means software built against an older version of a library (like glibc 2.17) will continue to work on systems with newer versions (like glibc 2.35). This is the opposite of backward compatibility. By building conda packages against the oldest supported glibc, they remain portable across a wide range of modern Linux distributions without recompilation.

Why does the libc boundary matter?

The C runtime library (glibc on Linux, libSystem on macOS, MSVC runtime on Windows) is the bridge between user-space software and the OS kernel. How a packaging system handles it determines portability, size, and host integration.


Shared techniques with Nix: prefix-relocatability and patching

Both conda packages and Nix instrument builds so binaries are prefix-aware and relocatable:

  • RPATH/RUNPATH patching so ELF binaries load from $PREFIX/lib
  • Shebang rewriting so scripts execute with the environment's interpreters
  • Mach-O (macOS) fix-ups (install_name_tool) for LC_LOAD_DYLIB/LC_ID_DYLIB
  • ELF surgery via patchelf, which originated in the Nix ecosystem and is widely used by conda-build and rattler-build.

The result is the same idea in both worlds: build products that bind to their own prefix, making environments movable and reproducible. Unlike conda’s goal of relocatable prefixes, Nix intentionally uses content-hashed, fixed store paths to guarantee purity. Relocation is not a design objective there.


Why not shipping glibc keeps environments small (and portable)

Conda's design philosophy of not shipping glibc (or the OS C runtime) has big consequences:

  1. No base OS duplication. Containers/Nix closures ship libc, ld-linux, locales/NSS, and friends. That's tens to hundreds of megabytes before your app starts! Conda packages reuse the host libc, so environments don't carry that weight.

  2. Fewer cascading dependencies. Pulling in glibc drags a constellation of tightly-coupled runtime pieces (dynamic loader, NSS, timezone/locale data). Leaving that to the host avoids inflating every environment.

  3. Security & updates. Host OS updates to glibc apply automatically across conda environments.

    The conda package managers (conda, mamba, pixi) enforce compatibility using virtual packages (e.g., __glibc>=2.17) and by building against the oldest supported glibc for forward compatibility.

  4. Still safe and predictable. The solver includes the host fact (__glibc, __osx, __win) so you cannot accidentally solve to an incompatible set.

Net effect

Environments that are typically hundreds of megabytes smaller than container images with equivalent stacks, and far faster to create, copy, and cache, without giving up ABI coherence.


Conda vs. Pip, Docker, and Nix

Let’s place them side by side:

Aspectpip/npmCondaDockerNix
ScopeLanguage-specific librariesMulti-language user-space distributionFull OS filesystem imageWhole-system deterministic distribution
Unit of publicationWheel/sdist (library)Distribution package (build + run metadata)Layered imageDerivation output (hashed closure)
Binary compatibilityAssumes host system dependenciesShips user-space dependencies (except libc on Linux)Bundles its own libc/base userspaceShips its own glibc (Linux) + all dependencies (macOS uses system libc)
SolverPython dependency resolver (backtracking)Cross-language SAT-based solver (resolvo/libmamba/libsolv)No dependency solver (layer composition)Deterministic functional evaluation (derivation closure)
Provenancesdist + wheel metadataRecipe, config, rendered metadata (info/)Dockerfile + image historyDeclarative recipes (derivations)
Install permissionsUser-level (may need system headers)User-level (shared package cache)Root or rootless modesSingle-user or multi-user
Forward compatibilityNot guaranteedBuilt against oldest supported libc baselineDepends on base image refresh cadenceImmutable reproducible closure (no forward compatibility promise)

The conda ecosystem's scope is not limited to scientific computing. Channels also package systems and DevOps tooling (e.g. ripgrep, terraform, helm) alongside Python/R/C++ stacks, as we'll explore in Part 3. This breadth is part of what makes conda a true multi-language distribution.

One subtle but important distinction

Each conda package archive contains an info/ directory with rich metadata (index metadata, rendered recipe, build inputs, hashes). That embedded provenance exceeds what a wheel or an image layer stores inline. We’ll explore how tooling leverages this in Part 3.


Conda’s “middle path”

The conda ecosystem occupies a sweet spot in the packaging spectrum:

  • Compared to pip/npm: conda packages handle native dependencies (BLAS, CUDA, compilers, system libs) and multi-language stacks. Pip/npm cannot.
  • Compared to Docker: conda environments are lighter, faster to create, and usable without root privileges, while still providing consistency. You can still put a conda environment inside a Docker container when you need isolation.
  • Compared to Nix: the conda ecosystem is more approachable and cross-platform. Nix provides stronger whole-system reproducibility, but requires system-wide setup and a steeper learning curve. Conda works out of the box on Linux, macOS, and Windows.

The conda ecosystem is thus best seen as a user-space distribution that balances power and accessibility. This balance is exemplified by its approach to portability through virtual packages.


Portability via virtual packages

The conda package managers (conda, mamba, pixi) also have a unique mechanism: virtual packages. These reflect facts about your host machine and influence the solve.

  • __glibcglibc version (Linux)
  • __osx / __win → macOS or Windows versions
  • __cudaCUDA GPU driver version
  • __archspecCPU microarchitecture details for optimization
  • __linux / __unix → platform family indicators

These aren’t installed files, they’re ephemeral host facts injected into the solver. They constrain solutions for compatibility while keeping prefixes relocatable across machines that share the same baseline characteristics.


Takeaway

The conda ecosystem sits in the middle of the packaging spectrum, not a language registry like PyPI/npm, not a container runtime like Docker, and not a full system package manager like Nix, but something uniquely useful in between.

Here's why:

  • Lighter than Docker and Nix
  • More powerful than pip/npm
  • Portable and forward compatible thanks to careful build policies and the libc boundary

That's why conda feels like a distribution, because it is one, in user-space.


Up Next: Part 3 — Practical Power: Reproducibility, Automation, and Layering

In the final article, we’ll dive into how conda’s model translates into real-world advantages: reproducibility, provenance, automation with lockfiles and automated updates with Renovate, and the layered workflow of combining conda with pip/npm.