Deployment templates
The repository ships three deployment templates, so you do not have to run the setup steps by hand. Pick the one that matches your JupyterHub:
| Your JupyterHub | Use |
|---|---|
| The Littlest JupyterHub (TLJH) | scripts/install-positron.sh |
| Single-host docker-compose | scripts/docker/ template |
| Zero to JupyterHub (Kubernetes) | scripts/z2jh/ template |
All of them need a license file (license.lic) from Posit: proof of entitlement. Send email to academic-licenses@posit.co.
Positron spells the CPU architecture three ways: the download filename uses x64/arm64, the CDN path uses x86_64/arm64, and the activation directory uses x86_64/aarch64. The TLJH script and the docker-compose template derive all three from a single POSITRON_ARCH (x64 or arm64). On Zero to JupyterHub you set the activation directory yourself, in values.yaml.
All three templates target Positron Server 2026.09.0 and newer. If you are deploying Positron Server 2026.07.0 through 2026.08.2 instead, which validate a Hub-minted signing token, a former way of working, follow Verifier setup by hand. None of these templates automate that flow.
TLJH one-shot install script
scripts/install-positron.sh performs every step of the Get started guide in one run. It downloads and unpacks Positron Server, installs the license next to license-manager (mode 644), installs jupyter-positron-server in the user environment, generates the JupyterHub config that puts positron-server on the session PATH, and reloads TLJH.
Prerequisites
- An existing TLJH deployment. Run the script once, as root, on the TLJH host.
license.licon that host.
Usage
Run as root, and configure everything through environment variables:
Terminal
sudo POSITRON_VERSION=2026.09.0-256 POSITRON_ARCH=x64 \
LICENSE_SRC=./license.lic \
./scripts/install-positron.shThe script is self-contained. It needs no file other than your license, so you can copy this one script onto the host.
Common options
Every setting is an environment variable with a TLJH default. Run ./scripts/install-positron.sh --help to see them all with current values. The ones you reach for most often:
| Variable | Purpose | Default |
|---|---|---|
POSITRON_VERSION |
Positron Server release to install | 2026.09.0-256 |
POSITRON_ARCH |
CPU arch, either x64 or arm64 |
arm64 |
LICENSE_SRC |
Path to your license file | ./license.lic |
POSITRON_TARBALL |
Use a pre-downloaded tarball instead of the CDN | download |
POSITRON_SERVER_PKG |
jupyter-positron-server spec, from PyPI or a git+https URL |
jupyter-positron-server>=0.0.6 |
To install without network access to the CDN, download the tarball elsewhere and pass POSITRON_TARBALL=path/to/positron-server.tar.gz.
The script stops early with a clear message if the license is missing, so a forgotten file fails before any changes are made.
Verify the install
When it finishes, the script prints the check to run:
Terminal
# License status (license-manager requires root)
sudo /opt/positron-server/resources/activation/linux/<arch>/license-manager status<arch> is the activation directory name, either x86_64 or aarch64. Students can then launch Positron from the JupyterLab launcher.
docker-compose template
The scripts/docker/ directory is the Docker-native counterpart: a JupyterHub + DockerSpawner hub image and a single-user Positron image, wired together with docker-compose. It is a deployment template: copy and adapt it. Do not treat it as a finished product.
From the scripts/docker/ directory:
Terminal
# 1. Configure: copy the example env, then edit arch, version, and ports
cp .env.example .env
# 2. Add the license. Git-ignored, mounted read-only, never baked into an image
cp /path/to/license.lic secrets/license.lic
chmod 644 secrets/license.lic
# 3. Build both images. The single-user image is behind a build-only profile, so name it
docker compose build hub singleuser
# 4. Start the hub (run from this directory -- see "How it works" in the README)
docker compose up -dThen open http://localhost:8000, or your HUB_PORT. If the license is missing, the hub fails at boot with a clear message rather than at session-start time. DockerSpawner then bind-mounts license.lic straight into each spawned single-user container’s activation directory. positron-server validates it itself, the same as every other template here.
The template ships with DummyAuthenticator, which accepts any username and any password. Replace it with a real authenticator, such as OAuth, native, or LDAP, in hub/jupyterhub_config.py, and rebuild before you expose the hub to anyone.
See scripts/docker/README.md for the full configuration reference and how the license mount works.
Zero to JupyterHub (Kubernetes)
scripts/z2jh/ targets Zero to JupyterHub (the official JupyterHub Helm chart).
This template targets Positron Server 2026.09.0-256 and jupyter-positron-server==0.0.6, which read the license file directly from the activation directory.
There is no custom hub image. Nothing Positron-related runs on the Hub, so the chart’s stock hub image is used unchanged: no hub.image, no hub.extraVolumes. Everything lives in the single-user image, and the license is a Kubernetes Secret mounted into user pods.
Usage
Build the single-user image and load or push it where your cluster can reach it:
Terminal
cd scripts/z2jh
docker build -t positron-singleuser:v1 --build-arg POSITRON_ARCH=x64 \
-f Dockerfile.singleuser .
# local cluster
kind load docker-image positron-singleuser:v1Set the architecture directory in values.yaml to match your nodes, then install, passing the license at install time so it never lands in a values file or in git:
Terminal
helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ && helm repo update
helm upgrade --install positron jupyterhub/jupyterhub \
--version 4.4.1 --namespace jupyterhub --create-namespace \
--values values.yaml \
--set-file 'singleuser.extraFiles.license\.lic.stringData'=./license.lic \
--wait
kubectl -n jupyterhub port-forward svc/proxy-public 8080:httpTroubleshooting
Two things in values.yaml are easy to get wrong, and both fail with symptoms that point somewhere else entirely:
subPathis required. The license must mount at the full file path. Mounting at the directory replaces it, solicense-managerand the activation libraries disappear.values.yamlusessingleuser.extraFiles, which derivessubPathfor you.values-secret-alternative.yamldoes not.- The activation directory is architecture-specific:
x86_64on amd64 nodes,aarch64on arm64. Every tarball ships both, fully populated, so the wrong one looks entirely normal on inspection. Mixed-architecture node pools cannot be served by a singlemountPath. You need per-architecturesingleuser.profileListentries with node selectors.
See Troubleshooting for the symptoms and the diagnostic commands.
Verifying
license-manager status requires root. On TLJH you can sudo it. User pods run as uid 1000 with no privilege escalation, so here it cannot be run at all. Check the mount and what Positron itself reports instead:
Terminal
POD=$(kubectl -n jupyterhub get pod -l component=singleuser-server -o name | head -1)
kubectl -n jupyterhub exec $POD -c notebook -- \
ls -l /opt/positron-server/resources/activation/linux/x86_64/
kubectl -n jupyterhub logs $POD -c notebook | grep -i "license verified"A healthy listing keeps license-manager, librclient.so, librserver.so and license-manager.conf beside license.lic.
See scripts/z2jh/README.md for the full configuration reference.
Next steps
- Configuration covers environment variables and admin-enforced settings.
- Troubleshooting covers a session that does not start.