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.

Note

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.lic on 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.sh

The 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
Note

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 -d

Then 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.

Warning

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).

Important

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:v1

Set 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:http

Troubleshooting

Two things in values.yaml are easy to get wrong, and both fail with symptoms that point somewhere else entirely:

  • subPath is required. The license must mount at the full file path. Mounting at the directory replaces it, so license-manager and the activation libraries disappear. values.yaml uses singleuser.extraFiles, which derives subPath for you. values-secret-alternative.yaml does not.
  • The activation directory is architecture-specific: x86_64 on amd64 nodes, aarch64 on 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 single mountPath. You need per-architecture singleuser.profileList entries 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