# jupyenv: a Nix flake template for a reproducible JupyterLab

> jupyenv is a Nix based framework for declaring Jupyter environments, so JupyterLab, the classic notebook and the kernels are defined as Nix configuration rather than installed into a mutable Python environment. It suits a team that wants a notebook environment to rebuild identically on another machine, and it does not suit someone who needs a notebook running in ten minutes without a Nix toolchain.

**tweag/jupyenv** — Declarative and reproducible Jupyter environments - powered by Nix

- Repository: https://github.com/tweag/jupyenv
- Website: https://jupyenv.io/
- Stars: 747 · Forks: 152
- Language: Nix
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/tweag-jupyenv

## Notebooks as a Nix derivation rather than a pip install

jupyenv is a Nix based framework for the definition of declarative and reproducible Jupyter environments. The README is brief about it: this repository provides a Nix-based framework for the definition of declarative and reproducible Jupyter environments, and these environments include JupyterLab with configurable extensions, the classic notebook, and configurable Jupyter kernels.

The framing matters. Most people arrive at a notebook environment by installing packages into a virtual environment, and the definition of that environment lives in a requirements file or a lock file that only describes Python packages. jupyenv takes the Nix approach instead, where the whole environment, including the interpreter, the JupyterLab extensions, the notebook server and the kernels for each language, is declared as configuration and built by Nix.

That is a heavier commitment than a pip install, and it buys a specific property. Because the environment is a Nix derivation rather than a mutable directory, it is described by the flake, and the lock file pins the inputs. Two people on two machines who run the same flake get the same JupyterLab.

The project is MIT licensed, the primary language of the repository is Nix, the documentation site is jupyenv.io, and the last push was on 2026-10-18. There is one tag, v0.1.0 from 2023-01-31, which tells you the project is treated as a template and a module set rather than as a versioned library.

## Four commands from an empty directory to a running JupyterLab

The getting started section is the whole first use, and it is short enough to quote exactly. The snippet creates a new project directory, initializes the project with a flake template, enables a Python kernel, and starts the JupyterLab environment.

```shell
mkdir my-project
cd my-project
nix flake init --template github:tweag/jupyenv
nix run
```

There is no separate install step, no virtual environment to activate and no kernel to register by hand. nix flake init pulls the template, which writes a flake.nix and a flake.lock into the new directory, and nix run then builds the environment and starts JupyterLab inside it. The first run is a build, so it is slow, and the README says as much by saying that after some time you should see output in your terminal.

The output is the Jupyter server announcing itself, and the README shows the sample.

```shell
[I 2022-10-11 18:47:30.346 ServerApp] Jupyter Server 1.17.1 is running at:
[I 2022-10-11 18:47:30.346 ServerApp] http://localhost:8888/lab?token=8f2261a45601848bd79eda97d8d39c3d0f4978bc61fbe346
```

Your default web browser should open into the JupyterLab environment, and if it does not, the README says to use one of the suggested URLs. The server listens on port 8888, which is the Jupyter default, and the URL carries a token, which is the standard Jupyter mechanism for stopping anyone else from opening the session.

The template is a starting point, not a fixed configuration. The README points to the website for further instructions about the environment, the kernels and the extensions, and the repository carries a template directory, a modules directory, an examples directory and a docs directory, so the shape of the configuration is meant to be edited after the initialisation.

## One module per language, and one example per module

The most informative thing in the repository is the directory listing, because it shows exactly how the framework is organised.

The modules directory holds a kernel module for each supported language, and the README header displays a logo for each one: bash, c, elm, go, haskell, python, javascript, julia, nix, ocaml, postgres, r, rust, scala, typescript, zsh and dotnet. The examples directory mirrors that list with one example per language, from examples/bash through examples/zsh, which is the clearest sign of how the framework is meant to be used. You copy the example, or read it, and adapt it.

The presence of postgres among the kernels is the interesting one. A PostgreSQL kernel is not a Python kernel, it is a database client exposed as a notebook cell language, and its inclusion says something about the intent. This is not a tool for one programming language with a notebook attached; it is a way to have a notebook that speaks several languages, including a shell, inside one environment where all of them were declared by the same configuration.

That combination is what makes the JupyterLab extension configuration meaningful as well. If the extensions are configured in the same place as the kernels, then the notebook server, the editors and the language runtimes all come from one declaration, and a change to the configuration is a change to the flake rather than a set of manual steps on a machine.

The repository also carries a flake-module.nix file at the top level, next to flake.nix, config.nix, default.nix and shell.nix, which indicates that the project is consumed as a Nix module and can be composed with other flakes rather than only used as a template.

## Contributing through a dev shell and a pre-commit formatter

The contributing section is two sentences and a name, and the name matters: this project provides a development shell which you can enter with nix develop.

That is the standard Nix workflow, and it means a contributor does not need to install the toolchain separately, since nix develop drops them into an environment with the right tools already resolved. The .envrc file at the top of the repository is the direnv counterpart, so a shell that loads .envrc will enter the environment automatically.

The one instruction with a specific command is that you should run pre-commit run -all before submitting a pull request for review, and the README explains why: this will run a Nix formatter for consistency. So the formatting of the Nix code is enforced by a pre-commit hook rather than by a human reviewer, which is the practical reason to adopt the tool for a project whose files are configuration.

The rest of the repository supports the same workflow. There is a mkdocs.yml, so the documentation is built with mkdocs, and a docs directory to feed it. The pyproject file is a Poetry project named jupyter-nix at version 0.1.0 with Python 3.11 as its constraint, a testbook dependency and jupyterlab as a development dependency, and it builds with poetry-core. The presence of testbook, a notebook testing library, is a signal about how the project checks itself, since the interesting behaviour of a Jupyter environment is the behaviour of a notebook.

The other top level entries are the ones you would expect from a Nix project: flake.lock for pinned inputs, a poetry.lock, shell.nix, and a _typos.toml configuration.

## What this does not do for you

There are limits to the approach that are worth naming before an evaluation starts, and they are visible in the documentation rather than hidden.

The first is that the entry point requires a working Nix installation and the willingness to wait for a build. The first nix run is not instant, and it pulls a substantial closure. For a team that has adopted Nix across its infrastructure, that is a solved problem. For a data analyst who needs a notebook in the next ten minutes, it is a barrier with no shortcut in the documentation, since there is no pip install path for the environment itself.

The second is that the kernel set is fixed by which modules you enable. The languages listed are the ones with a module and an example, and a language without a module is not something you can add by hand without writing the module, so a team with an unusual runtime is on its own there. The postgres kernel shows that the framework is willing to go beyond Python, but the breadth is defined by what the project has written, not by what Jupyter can technically do.

The third is that the JupyterLab extensions are configured through the same mechanism, and the README explicitly defers to the website for instructions about extensions. That is a reasonable choice for a documentation site, and it does mean the framework's extension story is not readable from the repository alone.

And there is a versioning asymmetry worth noting. The only release tag is v0.1.0 from January 2023, yet the last push was on 2026-10-18. The project is being worked on, but the tags do not track that work, so pinning to a tag gives you a 2023 snapshot rather than the current state, and a consumer pinning to the flake reference in the getting started snippet, github:tweag/jupyenv, follows the branch instead.

## How this differs from a requirements file for the same notebook

The realistic alternative is the one most teams already have: a Python virtual environment with a requirements file, plus a pip install of jupyterlab, plus a kernel spec registered by hand for anything beyond Python. The difference in approach is what the declaration covers.

A requirements file describes a set of Python packages and the interpreter version, and it says nothing about the JupyterLab extensions, the notebook server configuration, the Node or Go or Rust toolchains, or the shell that the bash kernel runs. Every one of those is a step in a setup document, and every one of them is a place where two people end up with different environments.

jupyenv moves all of it into one Nix configuration, which means the notebook environment is built rather than assembled. The cost of that move is the toolchain requirement and the build time, and the benefit is that the environment is a value that can be committed, reviewed and rebuilt. When the environment is declared in the same place as the rest of a Nix based project, adding a kernel is a configuration change rather than an instruction, and the flake.lock records the exact inputs used.

There is a middle path worth mentioning. A team that has one notebook project and a lot of other Python work will find the requirements file route less disruptive, and can use it. A team whose notebooks are part of a reproducible research pipeline, where the analysis has to be re-run a year later on a different machine, is the group the Nix approach is built for.

## Conclusion

jupyenv fits a team that has already accepted Nix as its toolchain and wants a notebook environment defined in the same configuration as everything else, so a colleague can recreate the same JupyterLab and kernels from the flake rather than from a list of pip commands. It does not fit a data analyst who just needs a working notebook today, since the path runs through nix flake init and a first build. Before adopting it, read the modules directory and the documentation site for kernels and extensions, and check that the languages your users need have an example under examples, because each supported language is a separate module and a separate example rather than a generic fallback.

## FAQ

### How do I create a new jupyenv project?

The README gives four steps: create a directory with mkdir my-project, change into it, run nix flake init --template github:tweag/jupyenv to initialize the project from the template, and then run nix run. The template enables a Python kernel and starts the JupyterLab environment.

### Which Jupyter kernels does jupyenv support?

The repository holds a kernel module per language, and the README header lists bash, c, elm, go, haskell, python, javascript, julia, nix, ocaml, postgres, r, rust, scala, typescript, zsh and dotnet, with a matching example directory for each one under examples.

### What does jupyenv include in a Jupyter environment?

The README says the environments include JupyterLab with configurable extensions, the classic notebook, and configurable Jupyter kernels. The Jupyter server listens on the default port 8888 and its URL carries a token.

### How do I contribute to jupyenv?

Pull requests are welcome. The project provides a development shell you can enter with nix develop, and the README asks you to run pre-commit run -all before submitting, which runs a Nix formatter for consistency.

### What licence is jupyenv released under?

The project is licensed under the MIT License, and the README points at the LICENSE file for the details. The repository uses a flake template rather than a released package, and its only tag is v0.1.0 from 2023.

## Sources

- [License: MIT](https://github.com/tweag/jupyenv/blob/main/LICENSE)
- [Project website](https://jupyenv.io/)
- [README](https://github.com/tweag/jupyenv/blob/main/README.md)
- [Releases](https://github.com/tweag/jupyenv/releases)
- [tweag/jupyenv on GitHub](https://github.com/tweag/jupyenv)

---

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