Journal indexDockup / field note
Note / self-host-jupyterlab

How to Self-Host JupyterLab in 2026: Tokens, Kernels and Persistent Notebooks

Deploy JupyterLab with the right port, durable storage, TLS, authentication and backups. Troubleshoot when the proxy drops kernels' WebSockets in production.

The shortest JupyterLab demo proves that a process listens on port 8888. Production needs stronger evidence. It must pass this scenario even after the container has been replaced: log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook.

JupyterLab is being deployed for a clear purpose: browser notebooks next to data and compute. Its most common deployment trap is that the proxy drops kernels' WebSockets or mounted notebooks belong to root, so public URL handling and durable state receive the same attention as image startup.

Prove JupyterLab survives replacement

Inventory every durable artifact: notebooks, data, environments and reproducible dependency files. Mount /home/jovyan/work before bootstrap, write harmless sample data and replace the container to prove that path is actually persistent. Include configuration that changes how stored data is interpreted, not only the largest directory.

Set retention, copy backups off-host and run a clean-room restore. The JupyterLab drill is complete when notebooks, data and environment specifications return and a representative cell produces the expected result. If snapshots are part of the plan, use PITR versus snapshot guidance to document what each mechanism can recover.

Define success for JupyterLab first

Do not let the JupyterLab image choose production architecture by accident. The image supplies a process on 8888; storage, routing and external requirements still need deliberate lifecycles. The local runtime requirement is explicit data mounts and compute sized for notebook workloads. Keep its lifecycle explicit so moving JupyterLab between hosts does not silently change behavior.

The deployment is ready for deeper testing when it can log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook. Follow the transaction in logs and watch kernel RAM and CPU, data copies, model training and language-server processes rather than JupyterLab's web UI. Those observations reveal whether the current topology isolates the right component.

Five checks stronger than container health

The release record for JupyterLab needs facts, not “looks good.” Store the selected image digest, configuration checksum, public hostname and a timestamped result for: log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook. Use non-production sample data so the check can run after every deployment.

Prove two lifecycle events separately. A container replacement must preserve normal operation; a clean recovery must show that notebooks, data and environment specifications return and a representative cell produces the expected result. While the checks run, measure kernel RAM and CPU, data copies, model training and language-server processes rather than JupyterLab's web UI and retain the result as the expected envelope for this version.

Test a denied or invalid condition as well: submit harmless input near the resource or format limit associated with this boundary: the proxy drops kernels' WebSockets or mounted notebooks belong to root. JupyterLab should fail in a diagnosable way and should not overwrite healthy state. Return the valid condition, rerun the sample and attach the relevant redacted logs. Those artifacts give a future rollback decision concrete evidence.

Launch JupyterLab with observable defaults

The first container should be easy to delete and recreate. Keep data off the writable layer, bind port 8888 only where the proxy can reach it and pass configuration at runtime.

docker run -d \
  --name jupyterlab \
  --restart unless-stopped \
  -p 127.0.0.1:8888:8888 \
  -v jupyterlab-data:/home/jovyan/work \
  -e JUPYTER_TOKEN=replace-with-a-long-random-value \
  quay.io/jupyter/minimal-notebook:latest

Pin the image after the initial test. Read the earliest startup error rather than the final restart message, verify each mount with docker inspect, and follow logs while you log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook. That sequence distinguishes a bad image command from a dependency or permission problem.

Do not give JupyterLab the whole host

Close the bootstrap window as soon as the first trusted administrator exists. JupyterLab's concrete trap is disabling the token on an internet-facing notebook or mounting broad host paths; the safer boundary is to keep token authentication enabled, mount only intended data and do not expose a privileged host terminal casually.

Treat JUPYTER_TOKEN according to its JupyterLab role: keep sensitive values out of Git, document rotation effects and never substitute a public example in production. Private networking should carry dependency credentials, and roles inside JupyterLab should grant the smallest useful action. Keep sensitive request bodies and provider responses out of routine logs.

Test JupyterLab from outside the server

Choose the final JupyterLab hostname before users save callbacks or client settings, then route the notebook server through HTTPS with WebSocket support. The platform route should terminate TLS once and target private port 8888.

Run the acceptance transaction externally. If the client never reaches JupyterLab, use the SSL validation checklist for DNS and certificate checks. If the request reaches JupyterLab but the proxy drops kernels' WebSockets or mounted notebooks belong to root, stop changing proxy redirects and inspect the application-specific boundary instead.

Logs that answer the next question

Use log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook as the JupyterLab smoke test after every deployment. Its supporting metrics are kernel RAM and CPU, data copies, model training and language-server processes rather than JupyterLab's web UI; alert where those resources approach a point that degrades the user action.

The main change risk is that base-image packages, notebook extensions and environment files need a reproducibility test before upgrades. A safe release starts from a restorable snapshot and validates any one-way state change before traffic moves. When the proxy drops kernels' WebSockets or mounted notebooks belong to root, keep the failed container long enough to read its configuration and first error.

A Dockup deployment still needs an JupyterLab acceptance test

The platform layer for JupyterLab consists of port 8888, ingress, TLS, runtime configuration, storage and dependency reachability. Dockup can reproduce those pieces for its own infrastructure or a server the customer connects.

Then the operator finishes the product layer: route the notebook server through HTTPS with WebSocket support; enforce this access rule — keep token authentication enabled, mount only intended data and do not expose a privileged host terminal casually; and run “log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook”. Recording that test alongside the deployment avoids confusing automated provisioning with application readiness.

Frequently asked questions

What does JupyterLab need for a production deployment?

Route the JupyterLab container on port 8888 through one HTTPS origin. The local runtime requirement is explicit data mounts and compute sized for notebook workloads. Do not call JupyterLab ready until you can log in with a token, start a kernel, run a notebook cell, save output, reconnect the WebSocket and reopen the notebook.

Which JupyterLab data belongs in a backup?

Persist /home/jovyan/work and include notebooks, data, environments and reproducible dependency files in the same recovery manifest. A clean JupyterLab restore passes only when notebooks, data and environment specifications return and a representative cell produces the expected result.

Does JupyterLab require HTTPS behind a reverse proxy?

Use HTTPS for the public JupyterLab origin and keep port 8888 on the internal route. Apply the JupyterLab setting correctly: route the notebook server through HTTPS with WebSocket support. For JupyterLab, HTTPS protects credentials or user content in transit and keeps origin-sensitive client behavior consistent.

How should a JupyterLab upgrade be tested?

Restore current JupyterLab state into an isolated deployment, apply the candidate version and repeat its acceptance transaction. Pay particular attention because base-image packages, notebook extensions and environment files need a reproducibility test before upgrades. Keep the previous JupyterLab image until its data-migration and rollback boundary are understood.