Troubleshooting

When Positron does not start for a student, the cause is almost always in one of four places: the launcher extension in the user environment, the license file, the install itself, which covers architecture and version, or, on Positron Server 2026.07.0 through 2026.08.2, the Hub verifier service. This page is organized by symptom.

Note

On Kubernetes, license-mount problems are the usual cause. See Zero to JupyterHub (Kubernetes) specifics.

Where to look first

Most answers are in the logs.

Terminal
# Full Hub log, which covers spawn errors and service registration
journalctl -u jupyterhub --no-pager -n 200
Terminal
# Hub log
docker compose logs hub

# A student's single-user container, named by `docker ps`
docker logs <jupyter-USERNAME-container>

The single-user server log shows what jupyter-positron-server and positron-server reported at launch. Reach it from the JupyterHub UI under the student server, or from the spawner logs.

The Positron tile does not appear

jupyter-positron-server provides the tile through jupyter-server-proxy, in the user environment.

Check the user environment:

Terminal
/opt/tljh/user/bin/pip show jupyter-positron-server
/opt/tljh/user/bin/jupyter server extension list

Fixes:

  • Extension missing: install it in the user environment and enable the proxy.

    Terminal
    /opt/tljh/user/bin/pip install 'jupyter-positron-server>=0.0.5'
    /opt/tljh/user/bin/jupyter server extension enable --sys-prefix jupyter_server_proxy
  • Installed but no tile: confirm JSP_POSITRON_LAUNCHER_DISABLED is not set in the spawner environment. Any value hides the tile.

  • Restart the student server, by stopping and starting it from the JupyterHub UI. The extension is picked up on server start, not on Hub reload.

The tile is there, but Positron never loads

The launcher runs, and positron-server fails to start.

Check the browser tab first. When positron-server exits during startup, the tab shows a “Positron Server failed to start” page with the end of the log positron-server writes on its own. That log names the failing step, so read it before going to the single-user server log. The launch is retried about every 30 seconds, so once the cause is fixed, refreshing the tab is enough. No server restart is needed. A refresh that briefly shows a proxy error landed mid-retry. Refresh again.

If the tab instead sits on a spinner for two minutes and then shows could not start positron in time, positron-server never exited. It hung. Go to the single-user server log for that one.

Check the single-user server log for the failing step, then:

  • positron-server: command not found, or binary not found: positron-server is not on the session PATH. Confirm the spawner sets it:

    # TLJH: jupyterhub_config.py
    c.Spawner.environment = {
        "PATH": "/opt/positron-server/bin:" + os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin"),
    }

    In the docker template the binary is on PATH through the single-user image ENV, and the spawner does not override PATH.

  • A license error: see the section below that matches your Positron Server version.

License errors on 2026.09.0 and newer

These versions read license.lic from the activation directory and validate it with the bundled license-manager. Check the license first:

Terminal
/opt/positron-server/resources/activation/linux/<arch>/license-manager status

Common failures:

  • Permission denied on the license file: each session reads the file as the student, so a root-only mode such as 600 fails every session. Confirm the mode is 644.

    Terminal
    ls -l /opt/positron-server/resources/activation/linux/<arch>/license.lic
    chmod 644 /opt/positron-server/resources/activation/linux/<arch>/license.lic
  • Expired or altered license: license-manager status reports the reason. A license file that exists but does not verify stops the session rather than falling back to anything else. Request a replacement from academic-licenses@posit.co.

  • Wrong architecture directory: positron-server looks under resources/activation/linux/x86_64 on x64 hosts and resources/activation/linux/aarch64 on arm64 hosts. A license placed in the other directory is not found.

License errors on 2026.07.0 through 2026.08.2

These versions accept a signed token only, which the Hub verifier mints. The verifier runs as a managed JupyterHub service, so its output lands in the Hub log:

Terminal
journalctl -u jupyterhub --no-pager | grep -i positron-license
Note

This section covers a hand-rolled Verifier setup deployment. Neither scripts/install-positron.sh nor the scripts/docker/ template in this repository sets up the verifier anymore. Both target Positron Server 2026.09.0 and newer, and 2026.06.1 and older, which validate license.lic directly. See License errors on 2026.09.0 and newer instead if you deployed with one of those templates.

The verifier is not running or not registered

  • No mention at all: the service config was not loaded. Confirm that jupyterhub_config.py, or the generated positron-license.py in jupyterhub_config.d/, contains the c.JupyterHub.services block, then reload.

    Terminal
    tljh-config reload
    # or: systemctl restart jupyterhub
  • The service starts and then dies: confirm the command exists in the Hub environment.

    Terminal
    /opt/tljh/hub/bin/pip show jupyter-positron-verifier
    ls -l /opt/tljh/hub/bin/positron-verifier
  • Port conflict on VERIFIER_PORT, which defaults to 10101: another process holds the port. Change VERIFIER_PORT and update POSITRON_LICENSE_MINTING_ENDPOINT to match.

Mint calls fail

  • Signing key not readable: the verifier runs as the Hub service account and reads POSITRON_MINTING_KEY_FILE. Confirm the file exists and that account can read it.

    Terminal
    ls -l /etc/positron/signing-key.pem   # mode 600, owned by the Hub user
  • Minting endpoint unreachable or wrong URL: POSITRON_LICENSE_MINTING_ENDPOINT must point at the Hub-proxied service path, e.g., http://127.0.0.1:10101/services/positron-license/mint.

  • 403 or other authorization errors from the mint call: the service role is missing scopes. Confirm the c.JupyterHub.load_roles block grants the positron-license service read:users.

Note

Upgrading to Positron Server 2026.09.0 or newer removes this whole class of failure. See Move an existing deployment off the verifier.

Install-time failures

  • Download fails with curl 403 or 404: the POSITRON_VERSION and POSITRON_ARCH combination does not exist on the Content Delivery Network (CDN). Verify the version string against the Positron releases page, or download the tarball yourself and pass POSITRON_TARBALL=./positron-server.tar.gz.

docker-compose specifics

  • The hub container exits at boot reporting a file not found or empty: the license secret was not placed. Copy it into scripts/docker/secrets/. A missing bind-mount source makes Docker create an empty directory, which is why the hub checks at boot.

    Terminal
    cp /path/to/license.lic secrets/license.lic
    chmod 644 secrets/license.lic
    docker compose up -d
  • Single-user containers fail to spawn with a license error: confirm POSITRON_LICENSE_HOST_PATH (set from ${PWD} in docker-compose.yml) resolved to the right absolute path. docker compose config prints the resolved value. It must be a path on the Docker host, which is why docker compose needs to be run from scripts/docker/ itself.

Zero to JupyterHub (Kubernetes) specifics

Logs live in the single-user pod, and positron-server restart-loops on failure, so always use --tail:

Terminal
POD=$(kubectl -n jupyterhub get pod -l component=singleuser-server -o name | head -1)
kubectl -n jupyterhub logs $POD -c notebook --tail=200

-c notebook avoids the “Defaulted container” notice from the pod’s init container.

could not start positron in time

The browser shows a blank page with a spinner for about two minutes, then a 500 with could not start positron in time. The message implicates slowness, but in a Kubernetes deployment it is far more often the license mount. The two-minute delay is jupyter-server-proxy giving up.

Check the activation directory first:

Terminal
kubectl -n jupyterhub exec $POD -c notebook -- \
  ls -l /opt/positron-server/resources/activation/linux/<arch>/

If only license.lic is there, plus entries like ..data and a timestamped directory, the Secret was mounted at the directory instead of at the file path. Add subPath: license.lic and make mountPath the full file path, or switch to singleuser.extraFiles, which derives subPath for you.

If the directory looks healthy but Positron still fails, the license is probably in the wrong architecture directory. Both x86_64 and aarch64 ship in every tarball and both look complete, so this is invisible to inspection:

Terminal
kubectl -n jupyterhub exec $POD -c notebook -- \
  python3 -c "import platform; print(platform.machine())"

Whatever that prints is the only directory Positron consults.

No license key provided in the pod log

No license key provided. A license key is required to use Positron in a hosted
environment. Provide a license key with the --license-key or --license-key-file ...

Taken literally this suggests the license is missing, and following its advice leads nowhere. It appears whenever Positron cannot validate a license, which includes the case where the license is mounted and readable but the activation libraries beside it were masked. Work through the activation-directory checks above before touching --license-key-file.

Confirming the license is readable at all

Terminal
kubectl -n jupyterhub exec $POD -c notebook -- id
kubectl -n jupyterhub exec $POD -c notebook -- \
  head -c 30 /opt/positron-server/resources/activation/linux/<arch>/license.lic

id reports uid=1000(jovyan) gid=100(users), and the license appears as root:users with mode 0440. Note that license-manager status requires root: usable via sudo on The Littlest JupyterHub (TLJH), but not in user pods, which have no privilege escalation.

User servers fail to spawn

The single-user image’s jupyterhub must be compatible with the Hub’s. Compare:

Terminal
kubectl -n jupyterhub exec deploy/hub -- jupyterhub --version
docker run --rm --entrypoint pip <your-image> show jupyterhub

A patch-level difference is fine. A larger gap needs a pin in the image.

Extensions reinstall on every launch

Expected when singleuser.storage.type is none, because $HOME is ephemeral and Positron re-bootstraps ~/.positron-server/extensions each spawn. Set storage.type: dynamic so it happens once.

Still stuck?

Open an issue at the jupyter-positron-server issue tracker with the relevant Hub and single-user log excerpts, your Positron Server version, and which setup path you used.