How To Install Jupyter Notebook on Ubuntu 26.04 LTS

Install Jupyter Notebook on Ubuntu 26.04

Anyone who has managed a data science team’s infrastructure knows the pain of a poorly configured Jupyter server. One misconfigured token, one exposed port, and suddenly your production box is running arbitrary code for anyone who stumbles onto it. That’s not hypothetical—it’s happened often enough that Jupyter’s own documentation dedicates entire sections to server security.

Ubuntu 26.04 LTS, codenamed “Resolute Raccoon,” changes the calculus slightly for anyone installing Python tooling. The distribution continues enforcing the externally-managed-environment protection that started rolling out with 24.04, meaning the old habit of running a bare pip install notebook and calling it done simply won’t work anymore. If you try it, you’ll hit a wall—and that wall is intentional, not a bug.

This guide walks through installing Jupyter Notebook on Ubuntu 26.04 LTS the way a production system actually needs it set up: isolated from system Python, wired into systemd so it survives reboots, reachable securely over SSH tunneling rather than exposed raw on a public interface, and hardened against the kind of sloppy defaults that turn a convenient notebook server into an open door. Whether you’re spinning this up on a personal workstation for exploratory data analysis or provisioning a shared research server that a whole team will hit concurrently, the underlying principles don’t change—only the scale of the consequences does.

We’ll also cover what happens when things go wrong, because they will. Port conflicts, missing binaries, permission errors after a system update—these are the tickets that land on a sysadmin’s desk at 6 PM on a Friday, and having the fixes memorized (or at least bookmarked) saves everyone a bad evening.

Why the Old pip Install Method Breaks on Ubuntu 26.04

Ubuntu, following Debian’s lead, has locked down the system Python interpreter under PEP 668. This means any attempt to run pip install directly against /usr/bin/python3 throws an externally-managed-environment error. It’s not Jupyter’s fault, and it’s not a packaging bug—it’s Canonical protecting the OS from having its Python dependencies clobbered by user-installed packages that could break apt-managed tools relying on that same interpreter.

The fix isn’t to force it through with --break-system-packages, even though that flag technically works and shows up in a lot of quick tutorials floating around right now. Forcing system-wide installs is a habit that comes back to bite you during the next apt upgrade, when a dependency conflict silently breaks something unrelated—maybe a system utility that shares a Python library version. Production servers don’t get that luxury. The correct approach, and the one every experienced admin should default to, is a dedicated virtual environment.

Prerequisites Before You Start

Before touching any install commands, confirm a few basics. Skipping this step is how you end up debugging a “broken” Jupyter install that was never actually broken—it just never had the right foundation.

  • Ubuntu 26.04 LTS confirmed via cat /etc/os-release
  • Sudo privileges on the account you’re using
  • At least 2GB of free RAM if you plan to run anything beyond toy notebooks (data-heavy kernels balloon fast)
  • Outbound internet access to PyPI, since the venv method pulls packages live from the package index
  • A rough idea of whether this server will be accessed remotely, because that decision shapes the networking steps later

Run the update first. It’s tedious, but skipping it is how half of all “random” installation failures happen—an outdated package index serving you a stale venv package version that conflicts with something else already on disk.

sudo apt update && sudo apt upgrade -y

Step-by-Step: Installing Jupyter Notebook on Ubuntu 26.04 LTS

Step 1: Install Python Virtual Environment Support

Ubuntu’s desktop images usually ship Python 3 out of the box, but minimal server installs (the kind you’d actually run in production) often lack the venv module entirely. Install it explicitly rather than assuming it’s there.

sudo apt install python3-venv -y

This package bundles the wheel files pip needs to bootstrap itself inside a fresh environment, so you won’t separately need python3-pip at the system level for this workflow. That’s a small but meaningful detail—fewer system-level Python packages means less surface area for conflicts down the road.

Step 2: Create a Dedicated Virtual Environment

Isolation is the whole point here. Don’t install Jupyter directly into system Python, and don’t reuse an environment meant for something else—dependency hell in Python is real, and mixing notebook packages with, say, a Django project’s requirements is asking for a version mismatch six months from now.

python3 -m venv ~/jupyter-env
source ~/jupyter-env/bin/activate

Once activated, your shell prompt should show (jupyter-env) prefixed to the usual prompt. If it doesn’t, the activation didn’t take—check that you sourced the correct path.

Step 3: Upgrade pip and Install Jupyter Notebook

Old pip versions inside a fresh venv can trip over newer package metadata formats, so upgrade first.

python -m pip install --upgrade pip
python -m pip install notebook

Installing the notebook package from PyPI in 2026 pulls in JupyterLab as a dependency automatically. That’s a change from a few years back when Notebook and Lab were separate installs entirely. Practically, this means you get both the classic notebook interface and the tabbed Lab interface from a single install, and you can switch between them without touching your environment again.

Step 4: Verify the Installation

jupyter notebook --version
jupyter lab --version

You should see version numbers in the 7.x and 4.x range respectively, not a command not found error. If both commands resolve, the environment is healthy and ready for real use.

Step 5: Launch and Test

jupyter notebook --no-browser

The --no-browser flag matters more than it looks. On a headless server—which is what most production Jupyter deployments actually are—there’s no display to open a browser window on, and omitting the flag just produces a harmless but noisy error in the terminal. The command prints a local URL with an access token:

http://127.0.0.1:8888/tree?token=<long-token>

Copy that token somewhere safe temporarily. You’ll need it for the first login unless you configure a password, which we’ll get to shortly.

Running Jupyter Notebook as a systemd Service

A notebook server that dies the moment you close your SSH session isn’t useful for anything beyond quick one-off tests. For a server that needs to persist—say, a shared research box that a whole team will hit throughout the day—wire it into systemd so it survives logouts, crashes, and reboots.

Create a Working Directory

mkdir -p ~/notebooks

Keep this separate from your home directory clutter. Notebooks tend to multiply, and having a dedicated directory makes backups and permission management far simpler later.

Write the systemd Unit File

sudo nano /etc/systemd/system/jupyter.service

Paste the following, swapping your_username for the actual account that owns the environment:

[Unit]
Description=Jupyter Notebook Service
After=network.target

[Service]
Type=simple
User=your_username
WorkingDirectory=/home/your_username/notebooks
ExecStart=/home/your_username/jupyter-env/bin/jupyter notebook --no-browser --ServerApp.ip=127.0.0.1 --ServerApp.port=8888 --ServerApp.root_dir=/home/your_username/notebooks
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Notice ServerApp.ip=127.0.0.1. That binding is deliberate—it keeps Jupyter reachable only from the local host, which means the only way in from the outside is through an SSH tunnel. Binding to 0.0.0.0 might seem convenient for quick remote access, but it’s the single most common mistake behind compromised Jupyter servers found scanning the internet for open notebooks.

Enable and Start the Service

sudo systemctl daemon-reload
sudo systemctl enable --now jupyter.service

Check that it actually came up clean:

sudo systemctl status --no-pager --lines=3 jupyter.service

Confirm it’s actually listening where expected:

sudo ss -lntp | grep ':8888'

Expected output looks like LISTEN 0 128 127.0.0.1:8888 0.0.0.0:*—note that even though the raw socket table shows a wildcard in the last column, the actual bound address is localhost only, which is what matters here.

If you skipped setting a password, grab the token from the service logs:

sudo journalctl -u jupyter.service -n 50 --no-pager | grep -Eo 'http://127.0.0.1:8888/[^ ]+' | tail -n 1

For anything beyond a one-person test box, set a persistent password instead of relying on rotating tokens:

jupyter notebook password

This prompts for a password and stores a hashed version in the Jupyter config, so restarts don’t require pulling a fresh token from logs every single time.

Accessing Jupyter Remotely via SSH Tunneling

This is the part that separates a properly run notebook server from a liability. Since the service binds only to localhost, the only legitimate path in from a remote machine is an SSH tunnel.

From your local workstation—not the server:

ssh -L 8888:localhost:8888 your_server_username@your_server_ip

If port 8888 is already occupied locally, change the first number: ssh -L 8899:localhost:8888 ... and then browse to localhost:8899 instead.

Once the tunnel is up, open http://localhost:8888 in your local browser and authenticate with the token or password. There’s no certificate to worry about, no exposed HTTPS endpoint to maintain, and no additional firewall rule required on the server side beyond the SSH port that’s presumably already open. For teams that need this shared across multiple people, a reverse proxy with proper TLS termination behind something like Nginx is the next logical step—but that’s a separate hardening project, not a default requirement.

Performance Considerations for Production Notebook Servers

Notebook servers have a reputation for being lightweight, right up until someone loads a 10GB dataset into a pandas DataFrame and the kernel process balloons past available RAM. A few tuning habits actually matter here:

  • Monitor kernel memory with ps aux --sort=-%mem | grep ipykernel regularly if multiple users share the box
  • Set --ResourceUseDisplay.mem_limit or use nbresuse for visible memory pressure inside the notebook UI itself
  • Store notebooks and datasets on fast local SSD storage rather than network mounts—disk I/O latency compounds badly with iterative cell execution and autosave cycles
  • For CPU-bound workloads, pin kernel processes with taskset if the host runs other latency-sensitive services alongside Jupyter
  • Restart idle kernels periodically; Jupyter doesn’t reclaim memory from long-idle kernels on its own, and a cron job calling the Jupyter REST API to cull idle sessions saves resources on shared servers

None of this matters much for a single developer running local experiments. It matters enormously the moment three or four people share one server and someone’s unclosed notebook is quietly holding 4GB of RAM hostage.

Security Hardening Checklist

Security research into exposed Jupyter instances has repeatedly found unauthenticated notebook servers scanning as open on the public internet, some of them running arbitrary code execution for anyone who finds the URL. Don’t be that server.

  • Keep ServerApp.ip bound to 127.0.0.1 unless there’s a specific, deliberate reason to expose it wider
  • Always set a password via jupyter notebook password instead of relying on token-only auth for persistent servers
  • Never disable token or password authentication with --ServerApp.token='' on anything that isn’t a fully isolated sandbox
  • Restrict filesystem access to the --ServerApp.root_dir you actually intend users to browse
  • Keep the UFW firewall active and only open the SSH port needed for tunneling, not the notebook port itself
  • Avoid storing API keys or credentials directly in notebook cells—use getpass prompts or a proper secrets manager instead
  • Apply system updates regularly; Jupyter itself ships security patches periodically for its server components

That last point about secrets deserves emphasis. It’s shockingly common to find API keys, database passwords, or cloud credentials pasted directly into notebook cells during a debugging session, then forgotten and committed to a shared repo or left in a saved .ipynb file that gets backed up somewhere. Treat notebooks the same way you’d treat any script that touches production credentials.

Troubleshooting Common Jupyter Notebook Errors on Ubuntu 26.04

Error: externally-managed-environment

This is the most common issue anyone will hit trying to install Jupyter the “old” way. The fix is straightforward—create and activate a virtual environment before installing anything:

python3 -m venv ~/jupyter-env
source ~/jupyter-env/bin/activate
python -m pip install notebook

Error: jupyter: command not found

Usually means the virtual environment isn’t active in the current shell, or you’re in a fresh terminal session that never sourced it.

command -v jupyter
ls -l ~/jupyter-env/bin/jupyter

If the first command returns nothing, reactivate:

source ~/jupyter-env/bin/activate

Or bypass activation entirely for a one-off run:

~/jupyter-env/bin/jupyter notebook

Error: Port 8888 Already in Use

Modern Notebook builds typically just hop to the next available port automatically rather than failing outright. If you want a fixed port instead:

jupyter notebook --no-browser --ServerApp.port=8899

Check what’s currently squatting on the port:

sudo ss -lntp | grep ':8888'

Systemd Service Fails to Start

Usually a path mismatch in the unit file—double-check that ExecStart points to the actual venv binary path and that WorkingDirectory exists. Run journalctl -u jupyter.service -n 50 to see the actual failure reason rather than guessing.

Kernel Dies Unexpectedly

Often a memory exhaustion issue rather than an actual crash. Check dmesg | grep -i oom for out-of-memory killer activity—if the kernel process was OOM-killed, that’s a resource problem, not a Jupyter bug.

r00t is a Linux Systems Administrator and open-source advocate with over ten years of hands-on experience in server infrastructure, system hardening, and performance tuning. Having worked across distributions such as Debian, Arch, RHEL, and Ubuntu, he brings real-world depth to every article published on this blog. r00t writes to bridge the gap between complex sysadmin concepts and practical, everyday application — whether you are configuring your first server or optimizing a production environment. Based in New York, US, he is a firm believer that knowledge, like open-source software, is best when shared freely.

Related Posts