Installing JupyterLab on RHEL 9 for AI/ML Workloads

JupyterLab is an interactive development environment for working with notebooks, code, and data. It’s become the standard tool for machine learning and data science — you can prototype models, visualize datasets, and run GPU-accelerated training all from a browser-based interface. If you’re building a home lab or a dedicated ML server, JupyterLab is one of the first things you’ll want running.

This guide walks through installing and configuring JupyterLab on RHEL 9. If you’re interested in the hardware behind this setup, I covered the full rig build in a separate post. I always try to run things as securely as possible — dedicated service user, SELinux enforcing, SSL — while keeping it GPU-ready (CUDA 12.8, PyTorch) and low-maintenance (systemd, log rotation).

Prerequisites

  • RHEL 9 (or compatible like Rocky/Alma 9) server with root access.
  • Basic knowledge of SSH, dnf, and Linux file permissions.
  • Dedicated storage partition or directory for notebooks and datasets.
  • NVIDIA GPU with drivers installed (optional but recommended for ML workloads).

System Preparation

Update the system and install essential packages for building, Git, and downloads.

1
2
3
sudo dnf update -y
sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
sudo dnf install -y dkms kernel-devel-$(uname -r) kernel-headers-$(uname -r) git wget curl bzip2 gcc gcc-c++ make python3-devel openssl-devel
  • dkms, kernel-devel — NVIDIA driver auto-rebuild on kernel updates
  • git, wget, curl, bzip2 — cloning repos, downloading Miniconda, extracting archives
  • gcc, make, python3-devel — compiling ML extensions like flash-attn
  • openssl-devel — cryptography packages

Install Miniconda (System-Wide)

Grab the latest installer from Miniconda’s download page.

1
2
3
4
wget "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh" -O m.sh
sudo bash m.sh -b -p /opt/miniconda
echo 'export PATH="/opt/miniconda/bin:$PATH"' | sudo tee /etc/profile.d/miniconda.sh
source /etc/profile.d/miniconda.sh

Create ML Environment (GPU-Ready)

Create a Conda environment with Python 3.12 and JupyterLab, then add PyTorch built for CUDA 12.8.

1
2
sudo /opt/miniconda/bin/conda create -n ml python=3.12 jupyterlab ipywidgets matplotlib seaborn pandas numpy scikit-learn -c conda-forge --override-channels -y
sudo /opt/miniconda/envs/ml/bin/pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
  • Why CUDA 12.8? The RTX 5090 (Blackwell, sm_120) needs PyTorch builds for CUDA 12.8 or newer — older wheels like cu124 don’t include kernels for it. PyTorch no longer publishes to its conda channel, so it’s installed with pip inside the env.
  • Optional extras: JAX, TensorFlow.

Create Dedicated jupyter User

Run Jupyter as non-root for security.

1
2
3
4
sudo useradd -r -s /bin/bash -d /data/jupyter jupyter
sudo mkdir -p /etc/jupyter /data/jupyter /var/log/jupyter
sudo chown -R jupyter:jupyter /data/jupyter /var/log/jupyter
sudo chown -R jupyter:jupyter /opt/miniconda/envs/ml

Configure JupyterLab

Generate Config and Password

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
PASS=$(/opt/miniconda/envs/ml/bin/python -c 'from jupyter_server.auth import passwd; print(passwd("YourStrongPass123!"))')

sudo tee /etc/jupyter/jupyter_lab_config.py > /dev/null <<EOF
c.ServerApp.ip = '0.0.0.0'
c.ServerApp.port = 8888
c.ServerApp.open_browser = False
c.ServerApp.allow_remote_access = True
c.ServerApp.password = '$PASS'
c.ServerApp.certfile = '/etc/ssl/certs/jupyter.pem'
c.ServerApp.keyfile = '/etc/ssl/private/jupyter.key'
c.ServerApp.root_dir = '/data/jupyter'
c.ServerApp.runtime_dir = '/var/run/jupyter/runtime'
c.ServerApp.browser = ''
EOF

sudo chown root:jupyter /etc/jupyter/jupyter_lab_config.py
sudo chmod 640 /etc/jupyter/jupyter_lab_config.py

Self-Signed SSL

1
2
3
4
5
6
sudo openssl req -new -x509 -days 3650 -nodes \
  -out /etc/ssl/certs/jupyter.pem \
  -keyout /etc/ssl/private/jupyter.key \
  -subj "/CN=$(hostname -f)"
sudo chown root:jupyter /etc/ssl/private/jupyter.key
sudo chmod 640 /etc/ssl/private/jupyter.key
  • Access: https://your-server:8888/lab with password.

Systemd Service (Secure and Persistent)

Setting up JupyterLab as a systemd service means it starts automatically when the server boots. If the server reboots or the process crashes, it comes right back up — no need to SSH in and manually start the web interface every time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sudo tee /etc/systemd/system/jupyter.service > /dev/null <<EOF
[Unit]
Description=JupyterLab ML Server
After=network.target

[Service]
Type=simple
User=jupyter
Group=jupyter
Environment="PATH=/opt/miniconda/bin:$PATH"
ExecStart=/opt/miniconda/envs/ml/bin/jupyter-lab --config=/etc/jupyter/jupyter_lab_config.py
WorkingDirectory=/data/jupyter
Restart=always
NoNewPrivileges=yes
PrivateTmp=yes
RuntimeDirectory=jupyter
RuntimeDirectoryMode=700

StandardOutput=append:/var/log/jupyter/lab.log
StandardError=append:/var/log/jupyter/lab-error.log

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now jupyter
  • Note: You can add ProtectSystem=full to increase security, but it may cause issues.
  • Logs: /var/log/jupyter/lab.log and lab-error.log.
  • Verify: sudo systemctl status jupyter.

SELinux Fixes (Keep Enforcing)

Note: The paths below (like /data and /data/jupyter) are specific to my setup — I put everything under /data because that’s where I had the most storage for downloading models and datasets. Adjust these paths to match wherever you installed things.

If SELinux is enabled (getenforce = Enforcing), you’ll need to set up proper contexts so Jupyter can run and bind to a port. Instead of borrowing httpd_* types (which technically works but is a hack), give the data directory a home-directory context and build a small local policy for anything else that gets denied:

1
2
3
4
5
# Map the Jupyter data directory to home directory context
sudo semanage fcontext -a -e /home/<your-user> /data/jupyter

# Apply the contexts
sudo restorecon -R -v /data/jupyter

If you hit denials, check the audit log and generate a custom policy:

1
2
sudo ausearch -m avc -ts recent | audit2allow -M myjupyter
sudo semodule -i myjupyter.pp
  • For GPU: sudo semanage fcontext -a -t xserver_misc_device_t '/dev/nvidia*' && sudo restorecon -R -v /dev/nvidia*.

Troubleshooting

  • Read-only filesystem on /data/jupyter/.local/share:
  • Check mount: mount | grep /data (fix ro or noexec with sudo mount -o remount,rw,exec /data).
  • SELinux: Use user_home_t context.
  • Immutable: lsattr -d /data/jupyter (fix with chattr -i).
  • Quick test: sudo -u jupyter mkdir -p /data/jupyter/.local/share/jupyter/runtime.

  • Service start failure: Check /var/log/jupyter/lab-error.log or journalctl -u jupyter.

  • If ProtectSystem=strict: Change to full.
  • Runtime dir: Set c.ServerApp.runtime_dir = '/var/run/jupyter/runtime' and use RuntimeDirectory.

  • Jupyter logs: tail -f /var/log/jupyter/lab.log.

  • Manual test: sudo -u jupyter /opt/miniconda/envs/ml/bin/jupyter-lab --config=/etc/jupyter/jupyter_lab_config.py --debug.

Optional Enhancements

  • Multi-User (JupyterHub): conda install -c conda-forge jupyterhub.
  • Log Rotation: Add /etc/logrotate.d/jupyter for daily rotation.
  • Performance Tips: Use torch.cuda.amp, pip install flash-attn.
  • GPU Test: python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))".

Final Verification

  • Access: https://your-server:8888/lab with password.
  • GPU: nvidia-smi.
  • Environment: conda activate ml; jupyter lab.

This setup is GPU-ready, secure, and optimized for AI/ML. For multi-user or advanced features, extend with JupyterHub.