# Jupyter Docker Stacks: ready-to-run notebook images from quay.io

> Jupyter Docker Stacks ships prebuilt Docker images for JupyterLab, Notebook and the surrounding scientific Python stack. The value is in the image hierarchy and the tagged releases; the cost is image size and a fixed user model you inherit rather than design.

**jupyter/docker-stacks** — Ready-to-run Docker images containing Jupyter applications

- Repository: https://github.com/jupyter/docker-stacks
- Website: https://jupyter-docker-stacks.readthedocs.io
- Stars: 8,467 · Forks: 2,980
- Language: Python
- License: BSD-3-Clause
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/jupyter-docker-stacks

## The problem Jupyter Docker Stacks removes

Getting a Jupyter environment running is not hard. Getting the same Jupyter environment running on a laptop, a lab server and a JupyterHub node is the part that consumes time. You need a Python version, a set of scientific packages that agree with each other, a non-root user, a startup script, and a token or password flow. Jupyter Docker Stacks packages all of that into published images.

The README lists four things you can do with a stack image: start a personal Jupyter Server with the JupyterLab frontend (the default), run JupyterLab for a team using JupyterHub, start a personal Jupyter Server with the Jupyter Notebook frontend in a local Docker container, or write your own project Dockerfile. That last item is the one that matters most for teams. The images are designed to be inherited, not just consumed.

The audience is therefore two groups. The first is individual users who want `docker run` to produce a browser-accessible notebook. The second is platform operators who need a base image whose user, working directory and startup script are already decided, so their own Dockerfile only adds project dependencies.

## How the image hierarchy works

The Makefile in the repository lists the images in build dependency order: docker-stacks-foundation, base-notebook, minimal-notebook, scipy-notebook, r-notebook, julia-notebook, tensorflow-notebook, pytorch-notebook, datascience-notebook, pyspark-notebook, all-spark-notebook. This is a linear chain, and it explains why the images differ so much in size. Each image is built on the one before it and adds packages.

docker-stacks-foundation sits at the bottom. base-notebook adds the Jupyter Server and the JupyterLab frontend. minimal-notebook adds a minimal set of packages on top. From there the chain branches by discipline: scipy-notebook for the scientific Python stack, r-notebook and julia-notebook for those languages, tensorflow-notebook and pytorch-notebook for the two deep learning frameworks, datascience-notebook for the combination, and pyspark-notebook and all-spark-notebook for Spark workloads.

The practical consequence is that choosing an image is choosing a layer in this chain, and you pay for everything below it. If you need Spark, you get the scientific stack underneath it whether you use it or not. The repository's docs directory contains a selecting page that the README links to for this decision, which is the right place to start rather than guessing from the image names.

A second mechanism is the startup script. The README shows `start-notebook.py --ServerApp.root_dir=/home/jovyan/work` as the way to change the default directory, which means the container's entrypoint is a script that accepts Jupyter server arguments. That is how you change configuration without rebuilding: pass flags through the startup command.

## Installing and running a first notebook

The README assumes Docker is already installed and links to the Docker installation guide. It also notes that since 2023-10-20 the images are pushed only to Quay.io, and that older images remain on Docker Hub but are no longer updated. So the registry in every command is quay.io, not Docker Hub.

The first example pulls the scipy-notebook image at a specific tag and maps the container's port 8888 to host port 10000:

```bash
docker run -p 10000:8888 quay.io/jupyter/scipy-notebook:2026-07-28
```

After the image is pulled, the server logs print a token. You visit `http://<hostname>:10000/?token=<token>` in a browser to load JupyterLab, where hostname is the machine running Docker. The README notes that the container remains intact for restart after the server exits, so this form is not ephemeral.

The second example trades persistence of the container for persistence of your files. It runs an ephemeral container and mounts the current directory into the container:

```bash
docker run -it --rm -p 10000:8888 -v "${PWD}":/home/jovyan/work quay.io/jupyter/datascience-notebook:2026-07-28
```

The `--rm` flag makes Docker remove the container and its filesystem on exit, but changes to `~/work` survive on the host because that path is the mount. The `-i` and `-t` flags keep standard input open and attach a pseudo-TTY, which is what lets you see the server logs in the terminal and stop the server with Ctrl-C.

One detail catches people out. The README states that Jupyter's root_dir is `/home/jovyan` by default, so new notebooks are saved there unless you change the directory in the file browser. To change it, the README says to add the server argument to the previous command:

```bash
docker run -it --rm -p 10000:8888 -v "${PWD}":/home/jovyan/work quay.io/jupyter/datascience-notebook:2026-07-28 start-notebook.py --ServerApp.root_dir=/home/jovyan/work
```

Without that argument, notebooks written to the default directory land outside the mounted volume and disappear with the container.

## Switching the frontend with DOCKER_STACKS_JUPYTER_CMD

JupyterLab is the default frontend for all the images. The README says you can switch back to Jupyter Notebook, or launch a different startup command, by passing the environment variable `DOCKER_STACKS_JUPYTER_CMD=notebook` at container startup, or any other valid `jupyter` subcommand.

This is a cleaner arrangement than maintaining separate notebook and lab images, and it means a team that standardised on the classic Notebook interface can keep using these images. It also means the frontend is a runtime decision, not a build-time one. If you want the Notebook interface baked in, you still have to pass the variable at every container start, or set it in your own Dockerfile or orchestration config.

Note the asymmetry with the root_dir example above. Changing the frontend is an environment variable; changing the root directory is a server argument passed to the startup script. Both are documented, but they are not the same mechanism, and mixing them up is a common source of confusion when writing a compose file or a JupyterHub configuration.

## Image size and the tag you pin

The image chain is the main limitation. Because every image inherits from the ones below it, the discipline-specific images carry the whole scientific stack plus their own additions. On a workstation with a fast connection that is a one-time cost. On a cluster where every user pulls the image, or in a CI job that starts a fresh container per commit, it is a recurring cost that shows up as pull time.

There is no slim variant in the list. The smallest image in the chain is docker-stacks-foundation, and it is a foundation, not an end-user environment. If you want a minimal Jupyter container, these images are the wrong tool; you would be better served assembling your own from a Python base.

The tag scheme is the other thing to get right. The README states that containers are published for both x86_64 and aarch64 platforms, that single-platform images have either an `aarch64-` or `x86_64-` tag prefix such as `quay.io/jupyter/base-notebook:aarch64-python-3.13.14`, and that multi-platform images exist as well. Pinning a bare date tag gets you the multi-platform image. Pinning a prefixed tag gets you one architecture. If your deployment mixes architectures, the prefixed tags will fail on the wrong host, and the failure appears at pull time rather than at build time.

Because the images are rebuilt on a schedule, a pinned tag is a snapshot of package versions. That is a feature for reproducibility and a maintenance obligation for security updates. The repository has a CHANGELOG.md and a SECURITY.md at the top level, which are the places to check when deciding whether to move a pin forward.

## Where it fits against a plain Python base image

The obvious alternative is to write your own Dockerfile from a Python base image and install Jupyter yourself. The difference is what you inherit. With a plain Python base you choose the Python version, the package manager, the user, the working directory and the entrypoint, and you own the result. With Jupyter Docker Stacks you inherit a non-root user whose home is `/home/jovyan`, a startup script that accepts Jupyter server arguments, and a package set that has already been resolved against each other.

That inheritance is the product. If you have ever spent an afternoon resolving a conflict between two scientific packages, the value of a prebuilt image is obvious. If you have a strict internal base image policy, or you need a user account that matches your directory service, the inheritance is friction, and you will spend the same afternoon stripping it out.

There is a middle path the README points at directly: write your own project Dockerfile on top of a stack image. That keeps the user, startup script and package resolution, and limits your changes to project dependencies. The repository's examples directory contains docker-compose, make-deploy, openshift and source-to-image examples, which suggests the project expects to be deployed through orchestration rather than only through `docker run`.

## Licence and upgrade cost

The project is licensed under BSD-3-Clause, and the README carries the standard Modified BSD License badge. That is a permissive licence: you can use, modify and redistribute the images, including in commercial settings, provided you keep the copyright notice and licence text. The licence covers the image definitions and build tooling in this repository; it does not automatically cover every package installed inside the images, which carry their own licences. If you redistribute an image, check the licences of the packages you actually ship, particularly anything in the deep learning or Spark images. This is not legal advice.

Upgrade cost is tied to how you pin. Date tags such as `2026-07-28` are immutable snapshots, so an upgrade is a deliberate edit to a tag string in a compose file, a JupyterHub config or a CI variable. The repository's CHANGELOG.md is the record of what changed between builds. Nothing in the README describes an automatic upgrade path or a rollback mechanism, so if you need to revert a pin you do it by editing the tag back to the previous date, which requires that you know what the previous date was. Record it when you upgrade.

## Conclusion

Adopt Jupyter Docker Stacks if you want a working JupyterLab container without writing a Python environment Dockerfile, or if you run JupyterHub for a team and want a maintained base to build on. Do not adopt it if you need a small image, a non-Jupyter web frontend, or a user account you control: the images are large, JupyterLab is the default frontend, and notebooks live under /home/jovyan unless you set ServerApp.root_dir. Before rolling it out, check that the tag you intend to pin exists on quay.io for your CPU architecture, since single-platform images carry an aarch64- or x86_64- prefix while multi-platform images do not.

## FAQ

### What is Jupyter Docker Stacks?

It is a set of ready-to-run Docker images containing Jupyter applications and interactive computing tools, published under the quay.io/jupyter organisation. The README lists uses such as a personal Jupyter Server with JupyterLab, JupyterLab for a team via JupyterHub, and writing your own project Dockerfile on top of an image.

### Where are the Jupyter Docker Stacks images published?

The README states that since 2023-10-20 the images are pushed only to Quay.io, at quay.io/jupyter. Older images remain available on Docker Hub but are no longer updated.

### How do I run a Jupyter Docker Stacks image locally?

The README's first example pulls quay.io/jupyter/scipy-notebook at a date tag and maps the container's port 8888 to host port 10000 with `docker run -p 10000:8888`. You then visit http://<hostname>:10000/?token=<token> in a browser, using the token printed in the console.

### Can I use the classic Jupyter Notebook frontend instead of JupyterLab?

Yes. JupyterLab is the default, but the README says you can pass the environment variable DOCKER_STACKS_JUPYTER_CMD=notebook at container startup, or any other valid jupyter subcommand, to launch a different startup command.

### Which CPU architectures do the Jupyter Docker Stacks images support?

The README states that containers are published for both x86_64 and aarch64. Single-platform images carry an aarch64- or x86_64- tag prefix, for example quay.io/jupyter/base-notebook:aarch64-python-3.13.14, while multi-platform images do not.

## Sources

- [Issues](https://github.com/jupyter/docker-stacks/issues)
- [jupyter/docker-stacks on GitHub](https://github.com/jupyter/docker-stacks)
- [License: BSD-3-Clause](https://github.com/jupyter/docker-stacks/blob/main/LICENSE)
- [Project website](https://jupyter-docker-stacks.readthedocs.io)
- [README](https://github.com/jupyter/docker-stacks/blob/main/README.md)

---

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