Skip to content

TutorialsAI & LLM

How to install JupyterLab on a Linux server with systemd and HTTPS

Install JupyterLab in a Python virtual environment on Ubuntu or Debian, run it as a systemd service on localhost and reach it over an SSH tunnel or Caddy HTTPS.

  • Intermediate
  • 30 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Create a dedicated user and virtual environment
  3. Step 2 — Set a password
  4. Step 3 — Configure the server
  5. Step 4 — Run JupyterLab as a systemd service
  6. Step 5 — Connect to JupyterLab
  7. Option A: SSH tunnel
  8. Option B: HTTPS with Caddy
  9. Step 6 — Use an NVIDIA GPU (optional)
  10. Step 7 — Add extensions and extra kernels
  11. Back up and restore
  12. Update JupyterLab
  13. When a team needs JupyterHub
  14. Troubleshooting
  15. Blocking request with non-local 'Host' behind Caddy
  16. Invalid credentials after changing the password
  17. The kernel dies or keeps restarting
  18. Connection lost or kernel status stays at Connecting
  19. torch.cuda.is_available() returns False
  20. Next steps

JupyterLab is Project Jupyter's web-based environment for notebooks, code, terminals and data files. On a server it gives you a persistent workspace for data analysis and machine learning: long jobs keep running when you close the laptop, and the notebooks sit next to the data and, on a GPU server, next to the GPU.

This guide installs JupyterLab with pip in a Python virtual environment owned by a dedicated jupyter user, sets a hashed password and runs the server as a systemd service that listens on 127.0.0.1 only. You then reach it through an SSH tunnel or publish it over HTTPS with Caddy. GPU frameworks, extensions, extra kernels, backups and updates follow. JupyterLab is a single-user server; for teams, the guide points you to The Littlest JupyterHub at the end.

Prerequisites

  • A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13. JupyterLab 4.6 needs Python 3.10 or newer, and all four releases ship a newer Python 3.
  • A non-root user with sudo rights and SSH key login, see Secure a new Linux server and Set up SSH keys.
  • For HTTPS access: a domain name such as jupyter.example.com with an A (and optionally AAAA) record pointing at the server, and Caddy from Caddy reverse proxy. The SSH tunnel option needs neither.

The Jupyter project does not publish minimum hardware requirements; your notebooks decide what you need. The figures below are a conservative starting point for light data work, not official or benchmarked numbers:

ResourceMinimum (official)Suggested starting point
CPUNot published2 vCPU
RAMNot published4 GB; more for large data frames or models
DiskNot published20 GB plus your data sets

Step 1 — Create a dedicated user and virtual environment

A separate user without sudo rights limits what a notebook or the built-in terminal can do on the server. Install the Python tools, create the user and build the virtual environment as that user:

Bash
sudo apt update
sudo apt install python3 python3-venv
sudo useradd --create-home --shell /bin/bash jupyter
sudo -u jupyter -H python3 -m venv /home/jupyter/venv
sudo -u jupyter -H /home/jupyter/venv/bin/pip install --upgrade pip
sudo -u jupyter -H /home/jupyter/venv/bin/pip install jupyterlab
sudo -u jupyter -H mkdir -p /home/jupyter/notebooks
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter lab --version

The last command prints the installed version, for example 4.6.4. Installing into a virtual environment keeps JupyterLab and your libraries separate from the system Python, which Ubuntu and Debian manage with apt. Never add the jupyter user to the sudo or docker groups.

Step 2 — Set a password

Jupyter Server uses token authentication by default. A password is more practical for a long-running server, and once one is set, token authentication is not enabled by default. Run the official command as the jupyter user and enter a long password twice:

Bash
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter server password

The command stores only a hash of the password in /home/jupyter/.jupyter/jupyter_server_config.json. Changing the password later invalidates existing sessions after a server restart.

Step 3 — Configure the server

Create /home/jupyter/.jupyter/jupyter_server_config.py with the listen address, port and notebook folder:

Bash
sudo -u jupyter -H tee /home/jupyter/.jupyter/jupyter_server_config.py <<'EOF'
c.ServerApp.ip = "127.0.0.1"
c.ServerApp.port = 8888
c.ServerApp.open_browser = False
c.ServerApp.root_dir = "/home/jupyter/notebooks"
c.ServerApp.local_hostnames = ["localhost", "jupyter.example.com"]
c.ServerApp.trust_xheaders = True
EOF

What the settings do:

  • ip = "127.0.0.1" keeps the server off every public interface; the default is localhost too, but stating it protects you from a later edit.
  • root_dir is the folder JupyterLab shows in its file browser and where new notebooks are saved.
  • By default Jupyter blocks requests whose Host header does not point to the local machine (allow_remote_access is False), which protects against DNS rebinding. local_hostnames adds your Caddy domain to the accepted names; replace jupyter.example.com with your own or remove the line if you only use the SSH tunnel.
  • trust_xheaders lets Jupyter read the client address and scheme from the X-Forwarded-* headers Caddy sets.

Step 4 — Run JupyterLab as a systemd service

Bash
sudo tee /etc/systemd/system/jupyterlab.service <<'EOF'
[Unit]
Description=JupyterLab
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
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now jupyterlab

Check that the service runs and listens on loopback only:

Bash
systemctl status jupyterlab --no-pager
ss -tln | grep 8888
curl -I http://127.0.0.1:8888/lab

ss should show 127.0.0.1:8888, and curl should return a 302 redirect to the login page. Logs are available with journalctl -u jupyterlab -f.

Step 5 — Connect to JupyterLab

Option A: SSH tunnel

The tunnel needs no domain, certificate or open port and is the most private option. On your own computer, run:

Bash
ssh -L 8888:127.0.0.1:8888 youruser@203.0.113.10

Keep the session open, browse to http://localhost:8888 and sign in with the password from Step 2.

Option B: HTTPS with Caddy

Add a site block to /etc/caddy/Caddyfile, reload Caddy and keep ufw limited to SSH and web traffic:

Caddyfile
jupyter.example.com {
    reverse_proxy 127.0.0.1:8888
}
Bash
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Open https://jupyter.example.com. Kernels talk to the browser over WebSockets, which Caddy proxies automatically and without a read timeout, so long-running cells keep their connection. With Nginx, follow Nginx with Certbot and forward the Upgrade and Connection headers; Traefik also works. If you know the addresses you work from, restrict the site to them with a remote_ip matcher in Caddy.

Step 6 — Use an NVIDIA GPU (optional)

On a GPU server, install the NVIDIA driver first and confirm it with nvidia-smi. Then install your framework's CUDA build into the JupyterLab virtual environment. For PyTorch, open the PyTorch installation selector, choose Linux, Pip, Python and the CUDA version it recommends, and run the pip install command it shows with /home/jupyter/venv/bin/pip as the jupyter user. Check the result:

Bash
nvidia-smi
sudo -u jupyter -H /home/jupyter/venv/bin/python -c "import torch; print(torch.cuda.is_available())"

The second command prints True when PyTorch can use the GPU. Restart running kernels after installing new packages.

Step 7 — Add extensions and extra kernels

Most JupyterLab extensions are prebuilt Python packages: install them with pip into the same virtual environment, restart the service and list what is active. The example installs the project's Git extension, which also needs git on the server (sudo apt install git):

Bash
sudo -u jupyter -H /home/jupyter/venv/bin/pip install jupyterlab-git
sudo systemctl restart jupyterlab
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter labextension list

The JupyterLab documentation warns that installing an extension lets it execute arbitrary code on the server, the kernel and in the browser, so install only extensions you trust. Since JupyterLab 4 the built-in Extension Manager installs from PyPI. To show installed extensions without allowing new installs from the browser, change the ExecStart line of the service to /home/jupyter/venv/bin/jupyter lab --LabApp.extension_manager=readonly, then run sudo systemctl daemon-reload and restart.

To give a project its own libraries, create a second virtual environment and register it as a kernel, using the command from the IPython documentation:

Bash
sudo -u jupyter -H python3 -m venv /home/jupyter/envs/project
sudo -u jupyter -H /home/jupyter/envs/project/bin/pip install ipykernel
sudo -u jupyter -H /home/jupyter/envs/project/bin/python -m ipykernel install --user --name project --display-name "Python (project)"

The new kernel appears in JupyterLab's launcher after a browser reload.

Back up and restore

Your work lives in /home/jupyter/notebooks; settings, the password hash and workspace layouts live in /home/jupyter/.jupyter. Save the package list too, so you can rebuild the environment instead of copying it:

Bash
sudo mkdir -p /opt/backups
sudo -u jupyter -H sh -c '/home/jupyter/venv/bin/pip freeze > /home/jupyter/requirements.txt'
sudo tar czf /opt/backups/jupyter-$(date +%F).tar.gz -C /home/jupyter notebooks .jupyter requirements.txt

To restore on a new server, repeat Step 1 to create the user and environment, unpack the archive and reinstall the packages, then create the service from Step 4:

Bash
sudo tar xzf /opt/backups/jupyter-2026-10-09.tar.gz -C /home/jupyter
sudo chown -R jupyter:jupyter /home/jupyter
sudo -u jupyter -H /home/jupyter/venv/bin/pip install -r /home/jupyter/requirements.txt

If you created extra kernels, add .local/share/jupyter to the archive. Large data sets may belong in separate backups; either way, copy backups off the server.

Update JupyterLab

Read the JupyterLab changelog for the new version, take a backup, then upgrade inside the virtual environment and restart:

Bash
sudo -u jupyter -H /home/jupyter/venv/bin/pip install --upgrade jupyterlab
sudo systemctl restart jupyterlab
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter lab --version

Upgrade extensions the same way. After a major operating system upgrade that changes the Python version, the virtual environment stops working: delete /home/jupyter/venv, create it again as in Step 1 and reinstall with pip install -r /home/jupyter/requirements.txt.

When a team needs JupyterHub

JupyterLab authenticates one user and runs everything as one Linux account. When several people need their own logins, files and resource limits on one server, use JupyterHub. The Jupyter project's The Littlest JupyterHub (TLJH) is the official distribution for that case. Its documentation says it aims to support Debian and Ubuntu releases with current long-term support on amd64 or arm64, needs at least 1 GB of RAM and root access, should not be installed on a personal computer and is not supported inside a Docker container. Install it on a fresh server following its own guide rather than next to the setup from this tutorial.

Troubleshooting

Blocking request with non-local 'Host' behind Caddy

Jupyter rejects your domain as a non-local host name. Add it to c.ServerApp.local_hostnames in jupyter_server_config.py, exactly as you type it in the browser, and run sudo systemctl restart jupyterlab.

Invalid credentials after changing the password

Restart the service after jupyter server password, and make sure you ran the command as the jupyter user with sudo -u jupyter -H; otherwise the hash was written to another user's home folder.

The kernel dies or keeps restarting

Usually the server ran out of memory and the kernel process was killed. Check with sudo dmesg | grep -i -E 'killed process|out of memory' and journalctl -u jupyterlab, then load less data at once or move to a server with more RAM.

Connection lost or kernel status stays at Connecting

The WebSocket connection fails. Caddy handles it automatically; with Nginx, add the Upgrade and Connection headers. Browser proxy autodetection can also break WebSockets, as the Jupyter Server documentation notes.

torch.cuda.is_available() returns False

Either the NVIDIA driver is missing (nvidia-smi fails), or a CPU-only build was installed. Reinstall PyTorch with the CUDA command from the selector into /home/jupyter/venv, then restart the kernel.

Next steps

Frequently asked questions

Should I expose JupyterLab directly on port 8888?

No. Anyone who logs in gets a Python kernel and a terminal on your server. Bind JupyterLab to 127.0.0.1 and reach it through an SSH tunnel, or through a reverse proxy such as Caddy that adds HTTPS, and protect it with a strong password.

Can several people share one JupyterLab?

JupyterLab is a single-user server: everyone who logs in acts as the same Linux user and sees the same files. For a team, use JupyterHub, for example The Littlest JupyterHub, which gives each person their own account and server.

How do I use the GPU from notebooks?

Install the NVIDIA driver on the server, then install the CUDA build of your framework into the JupyterLab virtual environment with the command from the official selector, for example on pytorch.org. Check with torch.cuda.is_available() in a notebook.

How do I reset a forgotten JupyterLab password?

Run jupyter server password again as the jupyter user. It overwrites the stored hash in jupyter_server_config.json; restart the service afterwards so the new password takes effect and old sessions end.

Sources

ایجاد گذرواژه

Please confirm