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 sudo privileges.
  • A domain or subdomain with a DNS A record pointing to the server's public IP. This guide uses jupyter.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.