# conda-lock: reproducible conda environments from a lockfile

> conda-lock solves a conda environment once per platform and writes the exact package set to a lockfile, so the same environment can be recreated later without a fresh solve. It suits teams provisioning environments from gitops pipelines, and it asks you to keep the lockfile in step with the environment specification and to think about channel availability.

**conda/conda-lock** — Lightweight lockfile for conda environments

- Repository: https://github.com/conda/conda-lock
- Website: https://conda.github.io/conda-lock/
- Stars: 564 · Forks: 120
- Language: Python
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/conda-conda-lock

## What conda-lock actually locks, and why the solve happens early

Conda environment files are good at describing what you want and bad at reproducing what you got. The distinction is the whole reason this project exists. An `environment.yml` lists `python=3.9` and `pandas`, and the solver then picks versions and builds that satisfy every constraint in the channel at that moment. Run that solve in January and again in July and you can get different artifacts, and on a machine with a different platform you get different artifacts again.

conda-lock moves the solve to a deliberate moment. The library is described as lightweight, and it generates fully reproducible lock files for conda environments by performing a conda solve for each platform you ask for. The platform list is the input, so a single lockfile can cover `osx-64` and `linux-64` in one run.

The second benefit is the one that surprises people, and it comes from the way installation works. Because the lockfile already contains the resolved package set, the conda solver is not invoked when installing from it. Installation becomes a download of known artifacts rather than a search, which is faster, quieter, and, more importantly, deterministic.

The audience the README points at is a team working in a gitops style setup, using conda to provision environments in various places. That is the case where reproducibility pays for itself, because the whole point of provisioning in several places is that they should be identical. The project is published on PyPI and on conda-forge, and the documentation site lives at conda.github.io/conda-lock/.

## Installing conda-lock, and why the isolated options come first

Installation is a choice between five commands, and the README recommends the first two for a reason.

```bash
pipx install conda-lock
condax install conda-lock
pip install conda-lock
conda install --channel=conda-forge --name=base conda-lock
mamba install --channel=conda-forge --name=base conda-lock
```

`pipx` and `condax` are listed first because they install conda-lock into an isolated environment, and the README says the other options carry a risk of dependency conflicts. That is worth taking seriously rather than treating as a preference. conda-lock pulls in a vendored copy of Poetry's dependency solver, among other things, and installing it into the same environment as your project is a good way to turn a reproducibility tool into a source of irreproducibility.

The two conda options install into the `base` environment from conda-forge, which is the conventional choice if you want conda-lock available everywhere on a machine and do not mind it living in base. The pip options are the ones to avoid if the environment you are working in matters.

The package requires Python 3.10 or newer, and the project metadata classifies it as Production/Stable for a science and research audience with an operating system independent classifier. The `Dockerfile` in the repository is the minimal version of all this: a Miniconda base image, a `pip install conda-lock`, and an entrypoint set to `conda-lock`, so you can run the tool in a container without installing it on the host.

## Generating, installing and rendering a lockfile

The basic workflow has three steps, and the commands are short enough to read as a contract.

Generating a multi-platform lockfile names the source specification and the platforms. The `--update` flag is the one that changes the way you work over time, and it deserves a close look.

```bash
# generate a multi-platform lockfile
conda-lock -f environment.yml -p osx-64 -p linux-64

# optionally, update the previous solution, using the latest version of
# pydantic that is compatible with the source specification
conda-lock --update pydantic

# create an environment from the lockfile
conda-lock install [-p {prefix}|-n {name}]

# alternatively, render a single-platform lockfile and use conda command directly
conda-lock render -p linux-64
conda create -n my-locked-env --file conda-linux-64.lock
```

The `--update` flag names a package and re-solves using the latest version of that package that is compatible with the source specification. The rest of the environment stays put, which is the behaviour you want when a security advisory forces a bump.

The detail that matters most is what happens when a lockfile already exists: it is used as a constraint when regenerating. The README describes this as useful for adding new packages while keeping everything else locked. Without that constraint, regenerating a lockfile would re-solve the world and produce a large diff, and the practical consequence is that a lockfile only stays reviewable if it is updated in small steps.

The render path exists for the case where you want a plain conda command instead of conda-lock's own installer. `conda-lock render -p linux-64` produces a single-platform file, and you can then hand it to `conda create --file`. That is the escape hatch for environments where the conda solver is already available and trusted, and it is also what you would use to inspect what the lockfile actually contains.

## File naming, output kinds, and the pre-1.0 escape hatch

Output naming is configurable, which matters once a repository holds more than one lockfile.

By default conda-lock stores its output in `conda-lock.yml` in the current working directory, and that same file is used by default for render, install and update operations. A different filename is supplied with the `--lockfile` option.

```bash
conda-lock --lockfile superspecial.conda-lock.yml
```

Rendered `explicit` and `env` lockfiles get their own default names, `conda-{platform}.lock` and `conda-{platform}.lock.yml` respectively, and the template can be replaced with `--filename-template`.

```bash
conda-lock -k explicit --filename-template "specific-{platform}.conda.lock"
```

That `-k explicit` flag is also the migration path. If you used conda-lock before the 1.0 release that added unified lockfiles, you can still get the old per-platform behaviour by asking for the `explicit` output kind.

```bash
conda-lock --kind explicit -f environment.yml
```

This is the difference between a unified multi-platform lockfile and a set of single-platform ones, and it is worth understanding before you pick, because the choice affects how your CI matrix consumes the file. A unified lockfile holds every platform in one document, which is convenient to keep in one place and noisier to read. Explicit per-platform locks are what you feed to `conda create --file` on a given machine, and they are what the render command produces.

## Compound specs, channel overrides and the platforms key

conda-lock can build a specification from several files at once, which is how a project keeps shared and specific dependencies separate.

```bash
conda-lock -f base.yml -f specific.yml -p linux-64 -k explicit --filename-template "specific-{platform}.lock"
```

In that case all dependencies are combined, and the ordered union of all `channels` becomes the final specification. The ordering matters because it is the order conda itself uses to resolve, so the position of a channel in one file relative to a channel in another is preserved rather than sorted alphabetically.

Channels can be overridden on the command line, which is the usual way to pin a lockfile to conda-forge regardless of what the environment file says.

```bash
conda-lock -c conda-forge -p linux-64
```

The other way to set platforms is inside the environment file itself, using a nonstandard `platforms` key.

```yaml
# environment.yml
channels:
  - conda-forge
dependencies:
  - python=3.9
  - pandas
platforms:
  - linux-64
  - osx-64
  - win-64
  - osx-arm64  # For Apple Silicon, e.g. M1/M2
  - linux-aarch64  # aka arm64, use for Docker on Apple Silicon
  - linux-ppc64le
```

The precedence is stated plainly: platforms given on the command line with `-p` override the values in the environment specification, and if neither is provided, conda-lock falls back to a default set of platforms. A detail in that list is easy to miss. `osx-arm64` is for Apple Silicon laptops, while `linux-aarch64` is the one for Docker on Apple Silicon, where the container runs Linux on ARM rather than macOS. Getting that pairing wrong produces a lockfile for the wrong architecture.

The same idea applies to splitting dependencies by purpose. A nonstandard `category` key assigns everything parsed from one file to a category, and the default category is `main`.

```yaml
# dev-environment.yml
channels:
  - conda-forge
dependencies:
  - pytest
  - mypy=0.910
category: dev
```

## pip sections, private repositories and what gets written to disk

conda-lock can also lock the `dependencies.pip` section of an environment file, and it does so with a vendored copy of Poetry's dependency solver rather than calling out to pip's own resolver.

Private pip repositories are supported, but with a stated limitation: conda-lock only supports legacy PyPI repositories with basic auth, and the README notes that most self-hosted repositories, Nexus and Artifactory among them, use that style. They are configured in a way that parallels channels, with a `pip-repositories` key.

```yaml
channels:
  - conda-forge
pip-repositories:
  - http://$PIP_USER:$PIP_PASSWORD@private-pypi.org/api/pypi/simple
dependencies:
  - python=3.11
  - requests=2.26
  - pip:
    - fake-private-package==1.0.0
```

The environment variable syntax in the URL is not decoration. The README points at `docs/authenticated_channels.md` for the rules on what gets stored and how environment variable substitution works, and that section anchor is the one to read before committing a lockfile to a repository. A lockfile is a file that ends up in version control and in build artifacts, so whether a credential is expanded into it or left as a placeholder is a security question, not a formatting detail.

There is a second route for private repositories. The Poetry configuration file format is supported, with the file named `config.toml`.

```toml
[repositories.example]
url = "https://username:password@example.repo/simple"
```

The location of that file is not fixed, and the README gives a one line Python command that prints the configuration directory for it.

By default, private repositories are used in addition to `pypi.org`, and the documentation also describes a way to disable `pypi.org` entirely. That default matters for reproducibility. If a private index can satisfy a dependency, you have not pinned which source it came from, and a lockfile that mixes sources is harder to audit than one that names them.

## Licence, vendored code and the cost of the lockfile itself

The licence deserves a second look because the metadata and the repository field disagree in a way that will confuse anyone checking. The repository's licence is recorded as a custom licence that GitHub cannot classify, while the `pyproject.toml` declares `license = "MIT"` and lists an MIT classifier, and the project metadata includes a `license-files` list naming the top-level `LICENSE` together with a long series of vendored licence files under `conda_lock/_vendor/`, covering conda itself, Poetry, Poetry core, cleo, packaging, frozendict, distro, appdirs and others.

That vendored list is a fact about the design rather than a formality. conda-lock ships copies of conda's own solving code and of Poetry's dependency solver inside the package, which is what allows it to work without depending on a particular conda installation, and it is also why the licence file list is long. If your organisation needs to know exactly which third party code ships in the artefact, that list is the answer, and the `LICENSE` file is the authoritative text.

The cost side of a lockfile is that it has to be maintained. A lockfile that is never regenerated drifts away from the environment specification, and at that point it is worse than no lockfile, because it looks authoritative. The workflow that keeps it honest is the one the project documents: regenerate with `--update` naming a specific package, review the diff, and commit it alongside the change that required it. Because an existing lockfile acts as a constraint on the next solve, the diff stays small and reviewable, and that property is lost the moment somebody deletes the lockfile and starts over.

On maintenance, the project is active. The last push to the tree was on 2026-10-22, the newest release is v4.0.3 from 2026-10-11, and it is published on PyPI, on conda-forge and through a pre-commit hook setup, with a Dockerfile and a separate DockerfileTest in the repository.

## Conclusion

Adopt conda-lock if you provision conda environments in more than one place, in CI or in a gitops style pipeline, and need the same package set every time rather than whatever the solver picks on the day. Do not adopt it for a single developer environment, where the solve cost is small and the extra file is overhead. Before generating a lockfile, check that the channels you use are reachable from every machine that will install from it, and read `docs/authenticated_channels.md` if you use authenticated channels, because the rules about environment variable substitution decide whether credentials end up written into the lockfile.

## FAQ

### How do I install conda-lock?

The README lists five options and recommends `pipx install conda-lock` or `condax install conda-lock` because they install the tool into an isolated environment, avoiding the risk of dependency conflicts that comes with a plain `pip install`.

### What does conda-lock do that conda itself does not?

It performs a conda solve once per target platform and writes the resolved package set to a lockfile, and because the lockfile already contains that set, the conda solver is not invoked again when you install from it. Installing then becomes a download of known artifacts rather than a fresh solve.

### How do I add a new package to an existing conda-lock lockfile?

Regenerate with the `--update` flag naming the package, for example `conda-lock --update pydantic`, which uses the latest version compatible with the source specification. An existing lockfile is used as a constraint, so everything else stays locked and the diff stays reviewable.

### Can conda-lock lock pip dependencies too?

Yes. The `dependencies.pip` section of an environment file can be locked as well, using a vendored copy of Poetry's dependency solver. Private pip repositories are supported, but only legacy PyPI repositories with basic auth, which covers most self-hosted setups such as Nexus and Artifactory.

### How do I target multiple platforms in one conda-lock run?

Pass the platforms with `-p`, for example `conda-lock -f environment.yml -p osx-64 -p linux-64`, or list them in the environment file with the nonstandard `platforms` key. Command line platforms override the values in the environment specification, and if neither is given a default set is used.

## Sources

- [conda/conda-lock on GitHub](https://github.com/conda/conda-lock)
- [Issues](https://github.com/conda/conda-lock/issues)
- [Project website](https://conda.github.io/conda-lock/)
- [README](https://github.com/conda/conda-lock/blob/main/README.md)
- [Releases](https://github.com/conda/conda-lock/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/conda-conda-lock
