TLJH gives 1 to 100 users one shared Jupyter environment on one server
Simple JupyterHub distribution for 1-100 users on a single server
At a glance
- What is it?
- The Littlest JupyterHub is a BSD-3-Clause distribution that provides hosted Jupyter Notebooks for 1 to 100 users on a single server, aimed at people who do not consider themselves system administrators. It ships a tljh-config command, a bootstrap installer for cloud providers, and a documentation standard in which an undocumented feature does not exist.
- Who is it for?
- TLJH fits a teacher, workshop leader or small research group who wants each student or collaborator to have a working notebook environment with the same installed libraries, and who is willing to run one server. It does not fit a platform team that needs multi-node orchestration, per-user environment control or a large user count, and it does not fit a cluster with a scheduler, since the scope is explicitly one server.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 19 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The audience is stated as people who are not sysadmins
The Littlest JupyterHub distribution helps you provide Jupyter Notebooks to 1-100 users on a single server, and the README is direct about who that is for. The primary audience are people who do not consider themselves system administrators but would like to provide hosted Jupyter Notebooks for their students or users.
That framing is the whole design brief. It is a distribution rather than a library, so the unit of delivery is a working server rather than a package you compose, and the goal is that a person with no infrastructure experience can follow a tutorial and end up with a functioning multi-user notebook service.
The two properties the README highlights follow directly from that. All users are provided with the same environment, and administrators can easily install libraries into this environment without any specialized knowledge.
The shared environment is the interesting decision. The alternative, giving every user their own conda environment, is what a full JupyterHub deployment would let you do, and it multiplies the work of keeping the service healthy by the number of users. One environment for everybody means the packages your students need are installed once, and it means a broken package is one broken package rather than thirty broken environments. The cost is that users cannot install conflicting versions of the same library, which is exactly the right trade for a classroom and the wrong one for a research group with heterogeneous needs.
The scale is the other boundary. The 1-100 range is not a soft suggestion, it is the shape of the thing, and it is the constraint that makes the simple design possible.
Installation is a bootstrap script with per provider tutorials
The installation section defines the supported platform first. TLJH aims to support Debian and Ubuntu versions that have current Long-Term Support, on amd64 or arm64 CPU architectures, and other Linux distributions are not supported.
That is a narrow support matrix on purpose. A distribution that supports one family of distributions can make assumptions about the system package manager, the init system and the user creation, and it can then provide an installer that works rather than a set of instructions that need adapting.
The install path is a set of tutorials, and the README recommends a particular one. There are tutorials to create a new server from scratch on a cloud provider and run TLJH on it, for Digital Ocean, OVH, Google Cloud, Jetstream, Amazon Web Services and Microsoft Azure, and the invitation to contribute a tutorial for your favourite provider. These are recommended if you do not have much experience setting up servers.
The alternative is a tutorial to install TLJH on an already running server you have root access to, and the README says you should use that if your cloud provider does not already have a direct tutorial, or if you have experience setting up servers.
The repository layout explains the mechanism. The bootstrap/ directory holds the installer, and there is a tljh/ package with the implementation, an integration-tests/ directory, a tests/ directory, and a docs/ directory. The setup.py registers a single console script.
entry_points={
"console_scripts": [
"tljh-config = tljh.config:main",
]
}So the administrative surface of the system is one command, tljh-config, and that is consistent with the promise of not needing specialised knowledge.
An undocumented feature does not exist
The documentation section contains the clearest statement of the project's values, and it is a stronger claim than most projects make.
We place a high importance on consistency, readability and completeness of documentation. If a feature is not documented, it does not exist. If a behavior is not documented, it is a bug! We try to treat our documentation like we treat our code: we aim to improve it as often as possible.
And then the escalation, if something is confusing to you in the documentation, it is a bug. The project would be happy if you could file an issue about it, or even better, contribute a documentation fix.
That is a coherent position for a project whose users are not administrators. The failure mode of a tool like this is not that the code breaks, it is that the administrator does something reasonable that the code did not anticipate, and the documentation was the only thing that could have told them. Treating unclear documentation as a defect rather than as a rough edge is a direct answer to that failure mode, and the request for documentation contributions rather than just issue reports is the mechanism that keeps it true.
The documentation is built with Read the Docs, with a .readthedocs.yaml at the root and a documentation build status badge in the README. The support channels are a Discourse forum at discourse.jupyter.org under the jupyterhub and tljh categories, and a Gitter room, and the contributing guide lives in the same documentation tree.
The README also points to a blog post that explains the reasoning behind the project in more depth.
One configuration command, one Traefik proxy, one shared environment
The setup.py dependency list is short and it describes the shape of the service.
install_requires=[
"ruamel.yaml==0.18.*",
"jinja2",
"pluggy==1.*",
"backoff",
"filelock",
"requests",
"bcrypt",
"jupyterhub-traefik-proxy==2.*",
]The last entry is the one to look at, because it is where the design decision shows. jupyterhub-traefik-proxy is a JupyterHub proxy implementation that puts Traefik in front of the hub, and it is how the per-user notebook servers are exposed on one hostname through path based routing rather than through one port per user. That is what makes a single server viable for 100 users, since a port per user would exhaust the port range or require configuration on every one of them.
ruamel.yaml is the configuration library, and it is the reason tljh-config can offer a well behaved command line experience, since ruamel.yaml round trips YAML while preserving comments and ordering, so a configuration file edited by tljh-config still reads like a human wrote it. jinja2 is the template engine, used to render configuration files for the components being managed. pluggy is the plugin system, which is how extensions hook into the tljh-config command. backoff and filelock are for retry logic and for locking during operations that must not run concurrently, and requests is the HTTP client used by the installer, and bcrypt is for password hashing.
The pin on jupyterhub-traefik-proxy at 2.* and on ruamel.yaml at 0.18.* are exact-ish constraints, which is appropriate for a distribution whose job is to produce a working combination rather than to be flexible.
The package requires Python 3.9 or newer, and the version is 2.0.1.dev, so the source tree is a development state between releases.
The tooling is a modern Python stack, and the release process is scripted
The pyproject.toml is a good picture of the project's engineering standards, and it is a carefully chosen set of tools.
Formatting is handled by three tools that cooperate. black is the formatter, with target versions listed as py39, py310, py311, py312 and py313, and isort uses the black profile so the two agree on import ordering. autoflake handles the cleanup, with remove-unused-variables and remove-duplicate-keys enabled and ignore-init-module-imports set so that a module's re-exports are not stripped.
Testing is pytest with coverage. The addopts line is --verbose --color=yes --durations=10 --maxfail=1 --cov=tljh, and the --maxfail=1 is the interesting flag, since it stops the run at the first failure rather than letting the rest of the suite run, which makes a failing run faster to read. asyncio_mode is set to auto, so async test functions do not need an explicit marker. There is a filterwarnings entry that suppresses a specific coverage warning about a module bootstrap never being imported, which is a sign of the integration tests running a bootstrap in a subprocess.
The coverage configuration runs in parallel mode and omits the tests and integration-tests directories from the measurement, so the number reported is the number for the shipped code.
The release process uses tbump, a tool for bumping, committing and tagging versions. The configuration holds the GitHub URL, a version regex that parses major, minor, patch, a pre-release suffix of a, b or rc followed by digits, and a dev suffix, and the current version is 2.0.1.dev. The git configuration has a message template of Bump to {new_version} and a tag template.
The repository also has a .pre-commit-config.yaml at the root, a .flake8 file, a CHANGELOG.md, and a dev-requirements.txt for the development dependencies, which are kept separate from the runtime ones in setup.py.
Where TLJH is the wrong choice
The first wrong case is more than one server, or more than a hundred users. The scope is one server and 1 to 100 users, and the design choices follow from that. A single shared environment, a single Traefik proxy in front of one hub, and an installer that assumes one machine are all correct for that envelope and wrong outside it.
The second wrong case is per-user environment control. The shared environment is the feature that makes the tool simple, and it is the feature that has to go if your users need conflicting versions of the same library. A research group with people working on projects at different dependency sets is exactly the case where per-user environments are the point, and at that point you want a full JupyterHub with a conda or environment server, not TLJH.
The third wrong case is a managed cluster environment. The topics list education, and the tutorials are for cloud VMs and a single server with root access. A site with a Slurm scheduler, a Kubernetes control plane, or a strict institutional provisioning pipeline is a different problem, and the assumption that the administrator can install into the system Python and manage the whole box is what TLJH trades away for its simplicity.
The fourth wrong case is expecting a supported release. The repository has no GitHub releases, the version in the source tree is 2.0.1.dev, and the last push was on 2026-10-05. A team that needs a versioned artefact to deploy, rather than a branch to follow, has to look elsewhere.
How it differs from a full JupyterHub deployment
The comparison worth making is with deploying JupyterHub directly, and the difference in approach is the amount of infrastructure the administrator is asked to own.
A standard JupyterHub deployment involves a authenticator, a spawner, a proxy, a notebook or lab server image, and storage, each of which is a configuration file and a service, and the administrator has to decide on each. The Hub service itself runs as a service on a port, it is fronted by a proxy, and the per-user servers are spawned from an image that the administrator has to build and keep up to date. For a platform team this is a normal Tuesday, and for a teacher it is the reason they never got Jupyter set up for their class.
TLJH removes the decisions. The environment is shared, the proxy is included, the spawner is chosen for you, and the install script writes a working configuration on a supported operating system. The result is that the administrator's job is to install a package, add users, and install libraries, which is what the README promises.
The second difference is the upgrade path. With a standard deployment, upgrading means upgrading the Hub, rebuilding the user image, and reconciling the two, and the administrator is the one who has to know that the spawner configuration and the Hub configuration are compatible. With TLJH, the tljh-config command and the distribution's own upgrade path handle the reconciliation, because the combination was tested before it was shipped.
The third difference is the extension model. TLJH uses pluggy, so extensions can add subcommands to tljh-config, which is how a course-specific addon or an institution policy is added without forking the distribution. A standard JupyterHub has its own extension story, but the two are not the same mechanism, and a team with existing JupyterHub extensions is not the audience TLJH is written for.
Editorial conclusion
TLJH fits a teacher, workshop leader or small research group who wants each student or collaborator to have a working notebook environment with the same installed libraries, and who is willing to run one server. It does not fit a platform team that needs multi-node orchestration, per-user environment control or a large user count, and it does not fit a cluster with a scheduler, since the scope is explicitly one server. Before adopting it, check that you are on a supported LTS Debian or Ubuntu release on amd64 or arm64, since other distributions are not supported, and note that the version in the source tree is 2.0.1.dev with no GitHub releases published, the last push being on 2026-10-05.
Frequently asked questions
What is The Littlest JupyterHub and who is it for?
It is a distribution that helps you provide Jupyter Notebooks to 1 to 100 users on a single server. The primary audience are people who do not consider themselves system administrators but want to provide hosted notebooks for their students or users, with all users sharing the same environment.
Which operating systems does TLJH support?
It aims to support Debian and Ubuntu versions that have current Long-Term Support, on amd64 or arm64 CPU architectures, and other Linux distributions are not supported. The install tutorials cover Digital Ocean, OVH, Google Cloud, Jetstream, Amazon Web Services and Microsoft Azure, plus a path for an existing server you have root access to.
How do I configure a TLJH installation?
Through the tljh-config command, which setup.py registers as a console script pointing at tljh.config:main. The project also uses pluggy as a plugin system, which is how extensions add subcommands to that command.
What does TLJH use to expose per-user notebook servers?
The install_requires list in setup.py includes jupyterhub-traefik-proxy at 2.*, which puts Traefik in front of the hub so the per-user servers are routed through one host rather than one port each. The rest of the dependencies are ruamel.yaml for configuration, jinja2 for templating, pluggy, backoff, filelock, requests and bcrypt.
What license is The Littlest JupyterHub released under?
It is released under the 3 Clause BSD licence, with a LICENSE file at the root of the repository and the license field in setup.py reading 3 Clause BSD. The project is maintained by the Jupyter Development Team, with a Discourse forum for support and a contributing guide in the documentation.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jupyterhub-the-littlest-jupyterhub)Was this useful?