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
- Prerequisites
- Step 1 — Create a dedicated user and virtual environment
- Step 2 — Set a password
- Step 3 — Configure the server
- Step 4 — Run JupyterLab as a systemd service
- Step 5 — Connect to JupyterLab
- Option A: SSH tunnel
- Option B: HTTPS with Caddy
- Step 6 — Use an NVIDIA GPU (optional)
- Step 7 — Add extensions and extra kernels
- Back up and restore
- Update JupyterLab
- When a team needs JupyterHub
- Troubleshooting
- Blocking request with non-local 'Host' behind Caddy
- Invalid credentials after changing the password
- The kernel dies or keeps restarting
- Connection lost or kernel status stays at Connecting
- torch.cuda.is_available() returns False
- 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
sudorights 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.comwith 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:
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 2 vCPU |
| RAM | Not published | 4 GB; more for large data frames or models |
| Disk | Not published | 20 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:
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 --versionThe 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:
sudo -u jupyter -H /home/jupyter/venv/bin/jupyter server passwordThe 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:
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
EOFWhat the settings do:
ip = "127.0.0.1"keeps the server off every public interface; the default islocalhosttoo, but stating it protects you from a later edit.root_diris the folder JupyterLab shows in its file browser and where new notebooks are saved.- By default Jupyter blocks requests whose
Hostheader does not point to the local machine (allow_remote_accessisFalse), which protects against DNS rebinding.local_hostnamesadds your Caddy domain to the accepted names; replacejupyter.example.comwith your own or remove the line if you only use the SSH tunnel. trust_xheaderslets Jupyter read the client address and scheme from theX-Forwarded-*headers Caddy sets.
Step 4 — Run JupyterLab as a systemd service
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 jupyterlabCheck that the service runs and listens on loopback only:
systemctl status jupyterlab --no-pager
ss -tln | grep 8888
curl -I http://127.0.0.1:8888/labss 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:
ssh -L 8888:127.0.0.1:8888 youruser@203.0.113.10Keep 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:
jupyter.example.com {
reverse_proxy 127.0.0.1:8888
}sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableOpen 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:
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):
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 listThe 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:
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:
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.txtTo 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:
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.txtIf 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:
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 --versionUpgrade 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
- Serve local language models to your notebooks with Install Ollama.
- Generate images on the same GPU with Install ComfyUI.
- Read the Jupyter Server security documentation for token, password and identity options.
- Compare servers for notebooks on the Jupyter hosting page.
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
- jupyterlab.readthedocs.io/en/stable/getting_started/installation.html
- jupyterlab.readthedocs.io/en/stable/user/extensions.html
- jupyterlab.readthedocs.io/en/stable/getting_started/changelog.html
- pypi.org/project/jupyterlab-git
- jupyter-server.readthedocs.io/en/latest/operators/public-server.html
- jupyter-server.readthedocs.io/en/latest/operators/security.html
- jupyter-server.readthedocs.io/en/latest/other/full-config.html
- pypi.org/project/jupyterlab
- ipython.readthedocs.io/en/stable/install/kernel_install.html
- tljh.jupyter.org/en/latest/install/custom-server.html
- tljh.jupyter.org/en/latest/topic/requirements.html
- caddyserver.com/docs/caddyfile/directives/reverse_proxy