JupyterLab is a browser-based environment for notebooks, code and data, widely used for data analysis and machine learning. Running it on a remote server lets you use more CPU, RAM or a GPU than your laptop has, from any browser. In this tutorial you will install JupyterLab in a Python virtual environment on Ubuntu 24.04, run it as a systemd service under a dedicated user, protect it with a password, and publish it over HTTPS through Nginx.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 GB of RAM (more for real data work).
- A non-root user with
sudoprivileges. - A domain or subdomain with a DNS
Arecord pointing to the server's public IP. This guide usesjupyter.your_domain; replace it with your own. - Ports 80 and 443 reachable from the Internet.
Step 1 - Creating a dedicated user and installing JupyterLab
Jupyter lets anyone who logs in run arbitrary code as the user it runs under, so it should not run as your admin user and never as root. Create a separate account for it:
sudo useradd --create-home --shell /bin/bash jupyter
Ubuntu 24.04 does not allow pip to install into the system Python, so install the tools to create a virtual environment:
sudo apt update
sudo apt install python3-venv python3-pip
Create the virtual environment in the jupyter user's home and install JupyterLab into it:
sudo -u jupyter python3 -m venv /home/jupyter/venv
sudo -u jupyter /home/jupyter/venv/bin/pip install --upgrade pip
sudo -u jupyter /home/jupyter/venv/bin/pip install jupyterlab ipywidgets
Create the directory that will hold the notebooks:
sudo -u jupyter mkdir -p /home/jupyter/notebooks
Confirm the installation:
sudo -u jupyter /home/jupyter/venv/bin/jupyter lab --version
4.4.9
Step 2 - Setting a password and configuring the server
JupyterLab runs on top of Jupyter Server, which reads its settings from ~/.jupyter/jupyter_server_config.py. First set a login password. The command stores only a hash of it, in ~/.jupyter/jupyter_server_config.json:
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter server password
Enter password:
Verify password:
[JupyterPasswordApp] Wrote hashed password to /home/jupyter/.jupyter/jupyter_server_config.json
Choose a strong, unique password: anyone who has it can run commands on the server.
Now create the main configuration file:
sudo -u jupyter nano /home/jupyter/.jupyter/jupyter_server_config.py
c = get_config() # noqa
# Listen only on localhost; Nginx will handle public traffic
c.ServerApp.ip = "127.0.0.1"
c.ServerApp.port = 8888
c.ServerApp.open_browser = False
# Directory shown in the file browser
c.ServerApp.root_dir = "/home/jupyter/notebooks"
# Trust the X-Forwarded-* headers sent by Nginx
c.ServerApp.trust_xheaders = True
Binding to 127.0.0.1 means JupyterLab is never exposed directly; the only way in is through Nginx over HTTPS.
Step 3 - Running JupyterLab as a systemd service
A systemd unit starts JupyterLab at boot, restarts it if it crashes and collects its logs in the journal. Create the unit file:
sudo nano /etc/systemd/system/jupyterlab.service
[Unit]
Description=JupyterLab server
After=network.target
[Service]
Type=simple
User=jupyter
Group=jupyter
WorkingDirectory=/home/jupyter/notebooks
ExecStart=/home/jupyter/venv/bin/jupyter lab
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Because the service runs as jupyter, it finds the configuration in /home/jupyter/.jupyter automatically. Load the unit and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now jupyterlab
Check that it is running and listening on localhost only:
sudo systemctl status jupyterlab --no-pager
sudo ss -tlnp | grep 8888
● jupyterlab.service - JupyterLab server
Loaded: loaded (/etc/systemd/system/jupyterlab.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-25 10:20:11 UTC; 5s ago
LISTEN 0 128 127.0.0.1:8888 0.0.0.0:* users:(("jupyter-lab",pid=4121,fd=6))
If the service fails, read its log with sudo journalctl -u jupyterlab -n 50.
Step 4 - Configuring Nginx as a reverse proxy
Install Nginx:
sudo apt install nginx
Jupyter uses WebSockets to talk to running kernels, so the proxy must forward the Upgrade and Connection headers. Create a server block for your subdomain:
sudo nano /etc/nginx/sites-available/jupyter
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name jupyter.your_domain;
client_max_body_size 100M;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
}
}
client_max_body_size allows uploading files up to 100 MB through the browser, and the long proxy_read_timeout keeps idle kernel connections open. Enable the site, test the syntax and reload:
sudo ln -s /etc/nginx/sites-available/jupyter /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If UFW is active, allow HTTP and HTTPS (and make sure SSH stays allowed):
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw status
Step 5 - Enabling HTTPS with Let's Encrypt
Your password and notebook contents must never travel in plain text. Install Certbot with its Nginx plugin and request a certificate:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d jupyter.your_domain
Certbot validates the domain, adds the certificate to the server block, and offers to redirect HTTP to HTTPS; accept the redirect. The Ubuntu package also installs a systemd timer that renews certificates and reloads Nginx automatically. Test renewal with:
sudo certbot renew --dry-run
Now open https://jupyter.your_domain in your browser. You should see the Jupyter login page; enter the password from Step 2 and JupyterLab opens in the notebooks directory. Create a Python notebook and run a cell such as import sys; sys.version to confirm that the kernel starts. If the cell runs, WebSockets are working through the proxy.
Step 6 - Adding kernels for other environments
Installing every project's libraries into the JupyterLab environment quickly leads to conflicts. Instead, give each project its own virtual environment and register it as a kernel. Create one for a machine learning project:
sudo -u jupyter python3 -m venv /home/jupyter/envs/ml
sudo -u jupyter /home/jupyter/envs/ml/bin/pip install ipykernel numpy pandas scikit-learn matplotlib
Register it with Jupyter. The --user flag writes the kernel spec to the jupyter user's data directory:
sudo -u jupyter -H /home/jupyter/envs/ml/bin/python -m ipykernel install --user --name ml --display-name "Python (ml)"
List the kernels Jupyter knows about:
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter kernelspec list
Available kernels:
ml /home/jupyter/.local/share/jupyter/kernels/ml
python3 /home/jupyter/venv/share/jupyter/kernels/python3
Reload the browser tab and "Python (ml)" appears in the Launcher. To remove a kernel later, run jupyter kernelspec remove ml the same way.
On a server with an NVIDIA GPU and a working driver, you can create a GPU kernel the same way by installing PyTorch into its own environment (pip install torch, then register it with ipykernel), and check it from a notebook with torch.cuda.is_available().
Troubleshooting
502 Bad Gateway. Nginx cannot reach JupyterLab. Check sudo systemctl status jupyterlab and curl -I http://127.0.0.1:8888, which should return a 302 redirect to the login page. Then read sudo tail -n 20 /var/log/nginx/error.log.
Notebook opens but the kernel shows "Connecting" or "Disconnected". WebSocket traffic is not getting through. Confirm the map block and the Upgrade and Connection headers are in the site file, run sudo nginx -t and reload Nginx.
Login rejects the correct password. The hash is read from /home/jupyter/.jupyter/jupyter_server_config.json. If you ran jupyter server password as another user, the hash ended up in that user's home. Run it again with sudo -u jupyter -H and restart the service.
Kernels die while running a cell. This is almost always the kernel running out of memory. Watch it with free -h and check sudo journalctl -k | grep -i oom. Load less data at once, or move to a server with more RAM.
Conclusion
JupyterLab now runs as an unprivileged systemd service on Ubuntu 24.04, reachable only through Nginx over HTTPS and protected by a hashed password, with separate kernels per project. From here you can put the notebooks directory under version control with Git, schedule backups of /home/jupyter, or deploy JupyterHub if several people need their own isolated workspaces on the same server.
