How to Install JupyterLab on Ubuntu 24.04 — A Remote Data Science Environment
JupyterLab has become the default workbench for data scientists, ML engineers, quantitative researchers, and anyone who prefers to think in code cells rather than scripts. Running it on a VPS, instead of on your laptop, unlocks a dramatically different workflow: your notebooks keep running when you close the lid, your datasets live next to your compute, and you can tap into real GPUs without buying one. This guide walks you through installing JupyterLab on Ubuntu 24.04 end-to-end — from a clean server to an SSL-secured, systemd-managed, multi-user-ready environment.
Want the fast path? Our GPU Server plans come with CUDA drivers and PyTorch pre-installed. Launch a GPU VPS and be running notebooks in under two minutes.
Table of Contents
What is JupyterLab?
JupyterLab is the next-generation web-based IDE from Project Jupyter. It extends the classic Jupyter Notebook interface into a full-fledged workbench with a file browser, terminal, text editor, CSV viewer, image preview, extension system, and — of course — notebooks. A notebook is a document made of code cells and Markdown cells that you can execute one at a time, inspect the output of, and iterate on. It is the lingua franca of exploratory data analysis, machine learning experimentation, academic teaching, and scientific computing.
Under the hood, JupyterLab talks to a kernel that executes your code. The default is IPython (Python), but Jupyter supports more than 40 language kernels including R, Julia, Scala, Rust, and Go. When you run a cell, the kernel evaluates it, keeps the resulting state in memory, and streams output back to the browser. That persistent state is what gives notebooks their interactive feel: you can load a 10 GB DataFrame once and then ask a thousand questions of it without re-reading the file.
JupyterLab is backed by the NumFOCUS-sponsored Jupyter open source organization and is used everywhere from Kaggle competitions to NASA research pipelines to the curriculum of most university data science programs.
Why Run JupyterLab on a VPS?
If you already have a laptop, running JupyterLab on a remote server might sound like added complexity. In practice, a VPS-hosted environment solves several real problems that local setups struggle with:
No local resource contention. Training a model or running a large Pandas groupby locks up your machine. You cannot take a Zoom call while your laptop fans are screaming. On a VPS, the work happens on dedicated CPU, RAM, and storage that you are not also using for Slack, Spotify, and ten browser tabs.
Accessible from anywhere. Start a long-running job from your desktop in the morning, check on it from your phone at lunch, and wrap up from a laptop in the evening. The kernel stays alive across sessions. No syncing, no "it works on my machine."
GPU access without buying hardware. A used RTX 3090 still costs over $800. A GPU VPS gets you an NVIDIA A4000, A5000, or H100 for a monthly rental — and you can spin it down when you are not training.
Persistent environments. Your conda environments, installed packages, cached datasets, and trained model checkpoints live on the server. You do not lose them when you reinstall the OS, switch laptops, or spill coffee on the keyboard.
Data locality. If your dataset lives in S3, Postgres, or on another VPS in the same datacenter, running JupyterLab close to the data is orders of magnitude faster than pulling gigabytes over residential internet.
Collaboration. Invite a colleague to the same JupyterHub and share notebooks, environments, and even running kernels.
Prerequisites
Before you start, make sure you have:
- An Ubuntu 24.04 LTS VPS with at least 2 vCPU and 4 GB RAM for light analysis, 4 vCPU and 8 GB RAM for typical ML work, or a GPU Server for deep learning.
- Root or sudo access via SSH.
- Python 3.11 or newer (Ubuntu 24.04 ships with 3.12 by default — we will confirm).
- A domain name pointing to your server's IP address (required for SSL).
- Ports 80 and 443 open in your firewall for nginx, plus SSH on 22.
- For GPU work: an NVIDIA GPU and the matching driver + CUDA toolkit. Our GPU Server plans come pre-configured.
Step 1: Update the System and Install Python
SSH into your server and bring it up to date:
ssh root@your-server-ip
apt update && apt upgrade -yConfirm the Python version:
python3 --versionYou should see Python 3.12.x. Install the supporting packages — pip, venv, and build tools for any C-backed libraries you will compile later (numpy, scipy, etc.):
apt install -y python3-pip python3-venv python3-dev build-essential \
libssl-dev libffi-dev git curlStep 2: Create a Dedicated User and Virtualenv
Running JupyterLab as root is a bad idea — a malicious or buggy notebook cell would then run as root. Create a dedicated user:
adduser --disabled-password --gecos "" jupyter
usermod -aG sudo jupyter
mkdir -p /home/jupyter/notebooks
chown -R jupyter:jupyter /home/jupyterSwitch to that user and create an isolated Python environment:
su - jupyter
python3 -m venv ~/venv
source ~/venv/bin/activate
pip install --upgrade pip setuptools wheelFrom here on, every pip install runs inside ~/venv and never contaminates system Python. When you want to leave the environment, run deactivate.
Step 3: Install JupyterLab
Still inside the activated virtualenv, install JupyterLab and a few useful companions:
pip install jupyterlab notebook ipywidgets jupyterlab-lsp python-lsp-serveripywidgets enables interactive widgets (sliders, dropdowns) inside notebooks. The LSP packages add autocomplete, go-to-definition, and linting — a huge quality-of-life upgrade over vanilla JupyterLab.
Verify the install:
jupyter lab --versionYou should see a version string like 4.2.5.
Step 4: Generate a Config and Password Hash
JupyterLab reads its settings from a Python config file. Generate the default:
jupyter lab --generate-configThat creates ~/.jupyter/jupyter_lab_config.py.
Next, generate a password hash. Never put a plaintext password in the config:
python3 -c "from jupyter_server.auth import passwd; print(passwd())"Type your password twice. You will get output like:
argon2:$argon2id$v=19$m=10240,t=10,p=8$...long hash...Copy the full hash — you will paste it into the config in the next step.
Step 5: Configure for Remote Access
Out of the box, JupyterLab binds to localhost only. We need it to listen on all interfaces (nginx will proxy to it), disable token auth in favor of password auth, and allow the remote origin.
Open the config:
nano ~/.jupyter/jupyter_lab_config.pyFind and set (or add) these lines:
# ~/.jupyter/jupyter_lab_config.py
c = get_config()Network
c.ServerApp.ip = '127.0.0.1' # nginx will proxy; keep bound to loopback
c.ServerApp.port = 8888
c.ServerApp.open_browser = False
c.ServerApp.allow_remote_access = TrueAuth — use password, not token
c.ServerApp.password = 'argon2:$argon2id$v=19$m=10240,t=10,p=8$...your hash here...'
c.ServerApp.token = ''
c.ServerApp.password_required = TrueRoot notebook directory
c.ServerApp.root_dir = '/home/jupyter/notebooks'Trust the reverse proxy
c.ServerApp.allow_origin = 'https://jupyter.yourdomain.com'
c.ServerApp.trust_xheaders = TrueIdle kernel culling — reclaim memory from abandoned notebooks
c.MappingKernelManager.cull_idle_timeout = 3600 # 1 hour
c.MappingKernelManager.cull_interval = 300
c.MappingKernelManager.cull_connected = FalseNote onipbinding. If you are not putting nginx in front (not recommended, but possible for quick private-network use), changec.ServerApp.ipto'0.0.0.0'so JupyterLab is reachable from outside the server. With nginx in front, keep it on127.0.0.1— that way nothing on the public internet can talk to JupyterLab except through the TLS-terminated proxy.
Save and exit.
Step 6: Launch JupyterLab
Still inside the virtualenv, run:
jupyter labYou should see logs confirming it is listening on 127.0.0.1:8888. For a quick test from your laptop, SSH-tunnel to it:
ssh -N -L 8888:127.0.0.1:8888 jupyter@your-server-ipThen open http://localhost:8888 in your browser and log in with the password you just set. If the login works, stop the server with Ctrl+C — we are about to put it behind nginx and systemd so you never need an SSH tunnel again.
Step 7: nginx Reverse Proxy with SSL
nginx terminates TLS, serves your custom domain, and — critically — proxies WebSockets, which JupyterLab uses to stream kernel output. Without WebSocket support, cells appear to hang forever.
Exit back to the root/sudo user and install nginx and Certbot:
exit # leaves the jupyter user shell
apt install -y nginx certbot python3-certbot-nginxCreate /etc/nginx/sites-available/jupyter:
# /etc/nginx/sites-available/jupyter server { listen 80; server_name jupyter.yourdomain.com;# Certbot will rewrite this block for SSL; leave it for now. location / { return 301 https://$host$request_uri; } }
server { listen 443 ssl http2; server_name jupyter.yourdomain.com;
# SSL certs — Certbot fills these in on first run ssl_certificate /etc/letsencrypt/live/jupyter.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/jupyter.yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
# Large uploads (datasets, model checkpoints) client_max_body_size 2G;
# Main proxy location / { proxy_pass http://127.0.0.1:8888; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support — REQUIRED for kernel communication proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
# Long-running cells need long timeouts proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_connect_timeout 60; proxy_buffering off; } }
Enable the site and test:
ln -s /etc/nginx/sites-available/jupyter /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default # optional, removes the welcome page
nginx -t
systemctl reload nginxPoint your DNS A record for jupyter.yourdomain.com at the VPS IP, wait for propagation, then issue a certificate:
certbot --nginx -d jupyter.yourdomain.comCertbot rewrites the config with working SSL paths and sets up auto-renewal via the certbot.timer systemd unit. Verify renewal works:
certbot renew --dry-runStep 8: Run JupyterLab as a systemd Service
Right now JupyterLab dies when you close your SSH session. systemd fixes that. Create /etc/systemd/system/jupyterlab.service:
# /etc/systemd/system/jupyterlab.service [Unit] Description=JupyterLab After=network.target[Service] Type=simple User=jupyter Group=jupyter WorkingDirectory=/home/jupyter/notebooks Environment="PATH=/home/jupyter/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ExecStart=/home/jupyter/venv/bin/jupyter lab --config=/home/jupyter/.jupyter/jupyter_lab_config.py Restart=on-failure RestartSec=5
Security hardening
NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=read-only ReadWritePaths=/home/jupyter
[Install] WantedBy=multi-user.target
Reload systemd, enable the service on boot, and start it:
systemctl daemon-reload
systemctl enable --now jupyterlab
systemctl status jupyterlabLogs go to the standard journal:
journalctl -u jupyterlab -fOpen https://jupyter.yourdomain.com in your browser, log in with your password, and you have a production-ready JupyterLab.
Step 9: Install Common Data Science Packages
Switch back to the jupyter user, activate the virtualenv, and install the standard stack:
su - jupyter source ~/venv/bin/activate
pip install \ numpy pandas scipy \ matplotlib seaborn plotly \ scikit-learn \ statsmodels \ xgboost lightgbm \ jupyterlab-git
For deep learning, install whichever framework you use:
# PyTorch (CUDA 12.1 build — adjust for your CUDA version)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121TensorFlow
pip install tensorflowHugging Face transformers + datasets
pip install transformers datasets accelerateRestart the service so the new packages are picked up by freshly spawned kernels:
exit
sudo systemctl restart jupyterlabVerify inside a notebook:
import torch
print(torch.cuda.is_available(), torch.cuda.device_count())On a GPU server this should print True followed by your GPU count.
Conda as an Alternative to pip
If you work with packages that have heavy non-Python dependencies (geospatial stacks like GDAL, bioinformatics tools, certain ML libraries), Miniconda is often easier than pip because it ships prebuilt binaries of the native dependencies.
Install Miniconda as the jupyter user:
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh -b -p ~/miniconda3
~/miniconda3/bin/conda init bash
source ~/.bashrcCreate a dedicated environment and install JupyterLab inside it:
conda create -n ds python=3.12 -y
conda activate ds
conda install -c conda-forge jupyterlab numpy pandas scikit-learn matplotlib -yThen update the systemd ExecStart to point at /home/jupyter/miniconda3/envs/ds/bin/jupyter instead of the venv path. You can also register multiple conda environments as Jupyter kernels so a single JupyterLab instance can launch notebooks in any of them:
conda activate ds
python -m ipykernel install --user --name ds --display-name "Python (ds)"The kernel will now show up in the JupyterLab launcher.
JupyterHub for Multi-User Deployments
If more than one person needs access, do not give everyone the same password to your single-user JupyterLab. Use JupyterHub instead. JupyterHub spawns a separate JupyterLab server per user, with per-user file isolation, authentication against Linux PAM / OAuth / LDAP, and resource limits.
Overview of the architecture:
- Hub — the central process that authenticates users and tracks their servers.
- Proxy — routes traffic to the right user's notebook server.
- Spawners — start a per-user server (as a Linux process, a Docker container, or a Kubernetes pod).
- Authenticators — verify logins (PAM for Linux users, GitHub OAuth, Okta, etc.).
apt install -y nodejs npm
npm install -g configurable-http-proxy
python3 -m venv /opt/jupyterhub
source /opt/jupyterhub/bin/activate
pip install jupyterhub jupyterlab notebookGenerate a config:
jupyterhub --generate-config -f /opt/jupyterhub/jupyterhub_config.pyEdit /opt/jupyterhub/jupyterhub_config.py:
c.JupyterHub.bind_url = 'http://127.0.0.1:8000'
c.Spawner.default_url = '/lab' # land on JupyterLab, not classic notebook
c.Authenticator.allowed_users = {'alice', 'bob'}
c.Authenticator.admin_users = {'alice'}Run JupyterHub as a systemd service (similar pattern to Step 8) and point nginx at port 8000 instead of 8888. Each Linux user on the server becomes a JupyterHub user. For containerized per-user isolation, look at the DockerSpawner or, for serious scale, zero-to-jupyterhub-k8s on Kubernetes.
Security Hardening
JupyterLab executes arbitrary Python. If someone gets in, they have a shell. Treat it accordingly.
Firewall. Only expose ports 22, 80, and 443:
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enableStrong passwords, rotated. Use a password manager. Rotate the JupyterLab password every few months by regenerating the hash and updating the config.
VPN-only access (most secure). If the notebooks are for you and a small team, do not expose JupyterLab to the public internet at all. Set up WireGuard and bind nginx to the VPN interface:
listen 10.8.0.1:443 ssl http2;Now only clients with a VPN tunnel can reach JupyterLab.
Fail2ban. Install and enable the nginx-http-auth jail to throttle brute-force attempts:
apt install -y fail2banIdle kernel culling. Already configured in Step 5 — abandoned kernels with a DataFrame loaded will silently eat your RAM otherwise.
Keep JupyterLab updated. Security patches land regularly:
su - jupyter
source ~/venv/bin/activate
pip install -U jupyterlab notebook jupyter-server
sudo systemctl restart jupyterlabNever run as root. We used a dedicated jupyter user precisely for this reason. Do not undo that.
Backing Up Your Notebooks
Your notebooks and data live in /home/jupyter/notebooks. Back them up.
Option 1: Git. For the notebooks themselves, a private GitHub or Gitea repo is the simplest backup — and gives you version history for free. The jupyterlab-git extension installed earlier adds a Git panel inside JupyterLab so you can commit and push without leaving the browser. Use nbstripout to strip heavy output from notebooks before committing.
Option 2: restic to S3 or B2. For datasets and model checkpoints that are too large for Git:
apt install -y restic
restic -r s3:s3.amazonaws.com/my-bucket/jupyter init
restic -r s3:s3.amazonaws.com/my-bucket/jupyter backup /home/jupyter/notebooksDrop that in a nightly cron job. restic deduplicates and encrypts, so incremental backups of a changing dataset stay small.
Option 3: VPS snapshots. If you are on a VPS-Server.host plan, enable automated snapshots from your dashboard. One-click restore in case the server is lost.
Whichever you pick, test the restore. An untested backup is a hopeful guess.
Troubleshooting
"Kernel died, will restart automatically." Almost always out-of-memory. Check journalctl -u jupyterlab and dmesg | tail for OOM killer entries. Upgrade to a plan with more RAM, or use smaller batch sizes / chunked reads (pd.read_csv(..., chunksize=...)).
Notebook hangs on "Kernel starting...". WebSocket upgrade is failing at the proxy. Confirm the proxy_set_header Upgrade and proxy_set_header Connection "upgrade" lines are in your nginx config and that nginx -t passes. Also check that no CDN or Cloudflare proxy in front is blocking WebSockets — enable the "Web Sockets" toggle in Cloudflare's Network tab if you use it.
"Blocking Cross Origin API request" in the browser console. Set c.ServerApp.allow_origin to your actual domain and set c.ServerApp.trust_xheaders = True as shown in Step 5.
403 Forbidden on login. The password hash in the config is malformed — regenerate with passwd() and make sure the entire string including the argon2: prefix is copied.
ModuleNotFoundError inside a notebook, but the package is installed. The kernel is using a different Python than your shell. In a cell, run import sys; print(sys.executable) and confirm it points to /home/jupyter/venv/bin/python. If not, install the package into that specific interpreter: /home/jupyter/venv/bin/pip install <package> and restart the kernel.
GPU is not visible. Run nvidia-smi on the host to confirm the driver works, then in a notebook run import torch; print(torch.cuda.is_available()). If it is False, your PyTorch was built for the wrong CUDA version — reinstall with the correct --index-url from the PyTorch install matrix.
Uploads fail for large files. Bump client_max_body_size in nginx (we set 2 GB in Step 7) and restart nginx.
JupyterLab will not start after a pip install. A package pinned an incompatible version of a core dependency. Revert with pip install jupyterlab==<previous-version> or recreate the virtualenv from a known-good requirements.txt.
FAQ
Can I run both JupyterLab and the classic Notebook interface?
Yes — they share the same server process. Visit /tree for classic, /lab for JupyterLab, on the same URL.
How much RAM do I need? For notebooks that work with datasets under 1 GB, 4 GB of server RAM is comfortable. For typical Pandas / scikit-learn workflows on a few million rows, 8–16 GB. For LLM fine-tuning or 50+ GB datasets, 32 GB+ and a GPU.
Can I SSH into the server while JupyterLab is running? Yes. JupyterLab is just a process. SSH, tmux, and the Jupyter kernel coexist fine. The JupyterLab UI also has a built-in terminal if you prefer staying in the browser.
How do I share a notebook with a colleague?
For read-only sharing, render it to HTML with jupyter nbconvert --to html notebook.ipynb and host the HTML. For collaborative live editing, JupyterLab 4's real-time collaboration feature supports multiple cursors — enable with jupyter lab --collaborative. For team-wide access with separate logins, use JupyterHub.
Is this suitable for production ML inference? No. Notebooks are for exploration and training. For serving a trained model, export it and run it behind FastAPI, TorchServe, or vLLM. Notebooks as production endpoints are a classic anti-pattern.
Do I need Docker?
Not for a single-user setup — the virtualenv is enough isolation. For JupyterHub with untrusted users, Docker (via DockerSpawner) gives you proper per-user containment.
Can I use R or Julia instead of Python?
Yes. Install the R kernel with install.packages("IRkernel"); IRkernel::installspec() inside R, or the Julia kernel with using Pkg; Pkg.add("IJulia"). Both appear in the JupyterLab launcher alongside Python.
What about VS Code's remote notebooks?
VS Code can connect to a remote Jupyter server — it is a different frontend against the same kernel. You can run both: JupyterLab in a browser for exploration, VS Code for heavier editing. Point VS Code at https://jupyter.yourdomain.com with your password.
Next Steps
Now that JupyterLab is running on your VPS, here are natural next moves:
- Install Ollama on the same server and call local LLMs from a notebook with the
ollamaPython client. - Set up a Postgres database on a second VPS and connect to it from notebooks via SQLAlchemy — proper data-warehouse workflow without leaving Jupyter.
- Configure VS Code Server alongside JupyterLab if you want a full IDE for the times notebooks are the wrong tool.
- Schedule notebooks with Papermill to run parameterised reports on a cron.
- Upgrade to JupyterHub once you add a second user.
Happy notebooking.