Installing JupyterLab on RHEL 9 for AI/ML Workloads
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 updatesgit,wget,curl,bzip2— cloning repos, downloading Miniconda, extracting archivesgcc,make,python3-devel— compiling ML extensions like flash-attnopenssl-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/labwith 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=fullto increase security, but it may cause issues. - Logs:
/var/log/jupyter/lab.logandlab-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(fixroornoexecwithsudo mount -o remount,rw,exec /data). - SELinux: Use user_home_t context.
- Immutable:
lsattr -d /data/jupyter(fix withchattr -i). -
Quick test:
sudo -u jupyter mkdir -p /data/jupyter/.local/share/jupyter/runtime. -
Service start failure: Check
/var/log/jupyter/lab-error.logorjournalctl -u jupyter. - If
ProtectSystem=strict: Change tofull. -
Runtime dir: Set
c.ServerApp.runtime_dir = '/var/run/jupyter/runtime'and useRuntimeDirectory. -
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/jupyterfor 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/labwith 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.