KodeKloud 100 Days of MLOps – Day 2: Fix a Broken JupyterLab Server Configuration
Step-by-step walkthrough of Day 2 task in KodeKloud 100 Days of MLOps challenge: diagnosing configuration errors, enabling root access, setting correct IP bindings, and launching a JupyterLab server. .
Misconfigured infrastructure is one of the most common blockers in MLOps workflows. When team configurations prevent core services from launching, understanding how to inspect, diagnose, and repair configuration files is essential.
In Day 2 of the KodeKloud 100 Days of MLOps challenge, we tackle a broken JupyterLab Server Configuration for the xFusionCorp Industries data science team, resolving network binding issues, directory paths, and root privilege blocks.
Before get in to the task and sollution I would like to leave a note about where and how a Jupyter server configs can be broken.
When troubleshooting or setting up a JupyterLab / Jupyter Notebook server, configuration failures typically fall into 6 different reasons.
Common Jupyter Server Configuration Pitfalls (Corrected & Expanded)
1. Network & IP Bindings (c.ServerApp.ip, c.ServerApp.port)
These settings determine where Jupyter listens for incoming connections.
Binding to 127.0.0.1 or a specific IP instead of 0.0.0.0
c.ServerApp.ip = "127.0.0.1"
This only allows connections from the local machine. Inside Docker or Kubernetes, Jupyter runs in its own network namespace. A reverse proxy (Nginx, Traefik, Apache) or Docker port mapping cannot reach it.
As a result you will most likely get:
502 Bad GatewayConnection Refused- Browser never connects
Correct configuration
c.ServerApp.ip = "0.0.0.0"
0.0.0.0 means:
Listen on every available network interface.
This is almost always required for:
- Docker
- Kubernetes
- Cloud VMs
- Reverse proxies
For a purely local laptop installation where only you access Jupyter, 127.0.0.1 is perfectly fine and even slightly more secure.
Port Conflicts
Setting c.ServerApp.port to a port already used by another service, or mismatched with the host port mapping
Example:
c.ServerApp.port = 8888
If another application already uses port 8888:
Error: Address already in use
In containers another mistake is mismatch of what proxy expects and what containor expose.
Container
Jupyter -> 8888
Docker
-p 8000:8888
Nginx
proxy_pass http://container:8000;
The proxy expects 8000 while the container exposes 8888.
port_retries
f c.ServerApp.port_retries = 0 is set and the designated port is busy, the server crashes immediately instead of searching for the next open port.
Default config is as below.
c.ServerApp.port_retries = 50
If the designated port 8888 is busy it will automatically search and try other free ports like ,
8889
8890
8891
...
If configured as
c.ServerApp.port_retries = 0
the server immediately exits if the selected port is occupied.
This is useful in production because you generally want the service to fail rather than unexpectedly switch ports.
2. Authentication, Permissions & Security
Running as root
Jupyter intentionally refuses to run as root for security reasons. If c.ServerApp.allow_root = True (or —allow-root) is missing in a container running as root, the startup process immediately aborts. If you looked at my terminal in bottom of this post you’ll see the actual error massage I got from Kodekloud task. But Container images often run as root.
Example error:
Running as root is not recommended.
Use --allow-root to bypass.
Required configuration:
c.ServerApp.allow_root = True
or
jupyter lab --allow-root
In the task i used config file instead of bash because task was about the config file. And something I learned about this is the Better practice is running the container as a non-root user whenever possible.
Token & Cross-Site Request Forgery (XSRF) Mismatches is another thing to address lets break it down
Authentication Tokens
Default:
c.ServerApp.token = "<random-token>"
or in newer versions:
c.IdentityProvider.token = "<random-token>"
This protects access to the notebook.
This is not common but some developers disable it:
c.IdentityProvider.token = ""
This is acceptable only if another authentication layer already exists (for example OAuth, JupyterHub, corporate SSO, or a trusted reverse proxy). Exposing an unauthenticated Jupyter server to the internet is a serious security risk.
XSRF Protection
Jupyter protects against Cross-Site Request Forgery.
Default:
c.ServerApp.disable_check_xsrf = False
Normally in production this should remain enabled.
Problems appear mostly in proxied environments (e.g., Nginx, Kubernetes Ingress, or Cloudflare):
- reverse proxies rewrite headers incorrectly (Host, X-XSRFToken, Origin).
- cookies are stripped
- cross-origin requests are misconfigured
When XSRF checks fail, the UI opens, but functional operations break with HTTP 403 Forbidden.
- Save fails
- Execute fails
- WebSocket authentication problems
- HTTP 403 errors
To fix this you can Disable XSRF:
c.ServerApp.disable_check_xsrf = True
Disabling XSRF protection exposes your server to malicious web pages executing code on your instance if an authenticated user visits them.should only be used for trusted internal deployments or debugging. It should not be the first fix for browser issues.Fixing header and proxy configurations on your ingress layer is always the preferred long-term solution.
Filesystem Permissions
When Jupyter starts, it needs read and write access to its runtime directory (by default, ~/.local/share/jupyter/runtime/) to store essential session state and security files:
jpserver-<pid>.json: Contains connection details (port, URL, token) for active server instances.jpserver-<pid>-open.html: The auto-generated launcher file used to open the UI in a browser.notebook_cookie_secret(orjupyter_cookie_secret): The secret key used to sign browser session cookies for security.
Permission Failures (PermissionError)
If Jupyter lacks write permissions to this directory, it will throw a PermissionError or fail to initialize cookie secrets during boot.
Common Causes:
- Docker Mounts: Volumes mounted into containers owned by
rootwhile running the container under a non-root user. - Read-Only Filesystems: Immutable container roots or restricted sandbox environments.
- Mismatched
$HOME: Runningsudowithoutsudo -Eor having$HOMEpointing to a directory the current user doesn’t own. - Missing Runtime Path: The directory structure does not exist on disk.
The Fix
Ensure the directory exists and grant the running user full read/write access:
# Create the runtime directory path
mkdir -p ~/.local/share/jupyter/runtime
# Restore ownership and permissions for the current user
chown -R $USER:$USER ~/.local/share/jupyter
chmod -R u+rw ~/.local/share/jupyter
Why adding chown helps:
In Docker environments (especially when switching between root and non-root container users), chmod alone can fail if the directory is strictly owned by root. Including chown -R $USER:$USER makes the fix complete across all edge cases!
3. Working Directory (root_dir)
This determines what users see inside Jupyter.
Example:
c.ServerApp.root_dir = "/workspace/notebooks"
Non-Existent Workspace Directory
If the configured root notebook directory (e.g., /workspace/notebooks) does not exist on disk, Jupyter will fail during the initialization phase.
Typical Error Output:
FileNotFoundError: [Errno 2] No such file or directory: '/workspace/notebooks'
traitlets.traitlets.TraitError: The 'root_dir' trait of a ServerApp instance expected a directory, but '/workspace/notebooks' does not exist.
Unlike some frameworks, Jupyter does not auto-create missing notebook directories on startup. If the target path isn’t present, initialization halts immediately.
The Fix Always ensure the target directory exists on disk prior to launching the server:
mkdir -p /workspace/notebooks
Relative vs. Absolute Paths
Avoid setting relative paths for working directories because the resulting root directory will depend entirely on where you execute the jupyter launch command in your terminal.
Avoid Relative Configurations
c.ServerApp.root_dir = "./notebooks"
If you use a relative path like ./notebooks, launching the server from different directories produces different working spaces:
# Serves /home/user/notebooks
cd /home/user
jupyter lab
# Serves /tmp/notebooks
cd /tmp
jupyter lab
- Instead:
Always Use Absolute Paths Specify explicit, absolute paths in your configuration files to ensure consistent behavior across all environments, startup scripts, and systemd/Docker services:
c.ServerApp.root_dir = "/home/user/notebooks"
- Configuration Hierarchy, Traitlets, and Syntax Pitfalls
Jupyter relies on Traitlets a Python based configuration and type checking system to manage all server parameters. Understanding how Traitlets evaluate classes, handle syntax errors, and apply configuration precedence will save you hours of debugging.
1. Class Hierarchy: ServerApp vs. NotebookApp
Modern JupyterLab (v3/v4) is built on top of jupyter_server, which uses the ServerApp class for configuration. Older versions (Notebook v6 and earlier) used the legacy NotebookApp class.
Modern (Jupyter Server / JupyterLab 3+):
c.ServerApp.port = 8888
c.ServerApp.ip = '0.0.0.0'
c.ServerApp.root_dir = '/root/notebooks'
Legacy (Notebook v6 and earlier):
c.NotebookApp.port = 8888
c.NotebookApp.ip = '0.0.0.0'
c.NotebookApp.notebook_dir = '/root/notebooks'
While Jupyter Server provides temporary aliases for many legacy NotebookApp traits via notebook_shim, relying on NotebookApp in modern setups is discouraged as these shims are slated for removal in future major releases. Always prefer c.ServerApp.
2. Configuration Files Are Executable Python
Because jupyter_lab_config.py is an actual Python file, any syntax or runtime error will prevent the interpreter from loading the file, crashing the server before it even initializes.
Common Failure Modes: Unclosed Strings (SyntaxError):
c.ServerApp.ip = "0.0.0.0
Result: The Python parser fails on startup, throwing a SyntaxError: EOL while scanning string literal.
Undefined Functions/Variables (NameError):
# Accidentally calling an unimported or non-existent helper
setup_custom_hooks()
Result: Python throws a NameError: name ‘setup_custom_hooks’ is not defined, aborting the boot process completely.
3. Configuration Precedence Order
Jupyter loads configuration files from multiple filesystem levels in a strict hierarchy. Settings loaded from higher-priority levels override lower-priority settings.
Load Order (Lowest to Highest Priority):
1. System-wide Configs (/etc/jupyter/)
│
▼
2. Virtual Environment Configs ($VIRTUAL_ENV/etc/jupyter/)
│
▼
3. User-Level Configs (~/.jupyter/)
│
▼
4. Explicit Config File (--config /path/to/custom_config.py)
│
▼
5. Direct CLI Flags (--port=8888 --ip=0.0.0.0)
If a setting defined in ~/.jupyter/jupyter_lab_config.py isn’t applying, check if a CLI flag or explicit —config flag passed to the jupyter lab executable is overriding it.
Debugging Config Search Paths To view the exact directories Jupyter searches for configuration files on your specific system, run:
jupyter --paths
Example Output:
config:
/root/.jupyter
/root/code/ml-env/etc/jupyter
/usr/local/etc/jupyter
/etc/jupyter
5. Virtual Environments & Kernels
Wrong Executable Path
A common issue in Python environments occurs when packages are installed into a virtual environment (e.g., ml-env/), but the system-wide binary is launched instead.
- Incorrect:
/usr/bin/jupyter lab(uses system Python) - Correct:
/root/code/ml-env/bin/jupyter lab(uses virtual environment Python)
How to identify:
ModuleNotFoundErrororImportErrorwhen trying to import libraries that are installed inside your virtual environment.- Mismatched package versions between terminal and notebook sessions.
Verification
Check which executable is being invoked:
which jupyter
To guarantee execution from the intended virtual environment, invoke the binary using its absolute path:
/path/to/venv/bin/jupyter lab --config /path/to/jupyter_lab_config.py
Broken Kernels & Dead Environments
Jupyter manages execution environments via kernel specification files (kernel.json).
Example kernel.json:
{
"argv": [
"/opt/myenv/bin/python",
"-m",
"ipykernel_launcher",
"-f",
"{connection_file}"
],
"display_name": "Python 3 (myenv)",
"language": "python"
}
If the underlying virtual environment directory (/opt/myenv) is moved, renamed, or deleted, the JupyterLab server will still boot normally, but running any notebook using that kernel will fail.
Typical Error Signals
- UI Notification:
Kernel failed to startorKernel Died - Terminal Log:
FileNotFoundError: [Errno 2] No such file or directory: '/opt/myenv/bin/python'
Fixing Broken Kernels
List all registered kernels and their disk locations:
jupyter kernelspec list
Remove or re-install the invalid kernel spec:
# Remove the invalid spec
jupyter kernelspec uninstall <kernel_name>
# Register a new kernel spec from inside the active virtual environment
/path/to/venv/bin/python -m ipykernel install --user --name myenv --display-name "Python (myenv)"
6. Reverse Proxies & WebSockets
JupyterLab relies heavily on two HTTP communication mechanisms:
- Standard REST HTTP requests for static assets, tree navigation, and REST APIs.
- WebSockets (
ws:///wss://) for kernel execution, terminal emulation, and real-time state sync.
If a reverse proxy (like Nginx, Apache, or Traefik) is placed in front of Jupyter without proper WebSocket upgrade support, the static UI will load, but code execution will fail.
Client Browser ──> Nginx (Reverse Proxy) ──[ HTTP + WebSockets ]──> Jupyter Server
How to identify:
- JupyterLab UI loads normally, but the status indicator stuck on “Connecting…” or “Disconnected”.
- Opening a terminal inside JupyterLab results in a blank, un-interactive screen.
Required Nginx Configuration
Ensure the Nginx configuration includes explicit HTTP/1.1 upgrade directives:
location / {
proxy_pass http://127.0.0.1:8888;
# Force HTTP/1.1 for WebSocket support
proxy_http_version 1.1;
# Pass Upgrade headers for WebSockets
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Preserve host and client context
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Serving Under a Base Path (base_url)
When hosting JupyterLab behind a subpath (e.g., https://example.com/jupyter/), update c.ServerApp.base_url in your configuration file so asset URLs and API requests resolve correctly:
# jupyter_lab_config.py
c.ServerApp.base_url = '/jupyter/'
Without this setting, static assets (.js, .css) and API endpoints will return 404 Not Found errors when accessed through the proxy subpath.
7. TLS / HTTPS Offloading
In most production environments, SSL/TLS termination is handled by an ingress controller or reverse proxy (e.g., Nginx, Cloudflare):
Internet ──[ HTTPS ]──> Nginx ──[ Unencrypted HTTP ]──> Jupyter Server (0.0.0.0:8888)
In this setup, Jupyter runs over HTTP internally and does not require TLS certificates configured in its python configuration file.
Direct TLS Configuration (No Proxy)
If Jupyter handles TLS termination directly without a reverse proxy, specify the certificate paths in jupyter_lab_config.py:
c.ServerApp.certfile = '/etc/ssl/certs/jupyter.crt'
c.ServerApp.keyfile = '/etc/ssl/private/jupyter.key'
Common Failure: If the specified
certfileorkeyfilepaths do not exist or lack read permissions for the running user, JupyterLab will fail on startup.
8. Extension Compatibility Diagnostics
Incompatible or outdated extensions can cause server startup failures or prevent the frontend from rendering properly.
Inspecting Installed Extensions
To check active server extensions:
jupyter server extension list
To check active frontend extensions:
jupyter labextension list
Troubleshooting Extension Errors
If an extension causes crash loops or crashes the server, disable or uninstall it:
# Disable a server extension without uninstalling
jupyter server extension disable <extension_name>
# Uninstall a lab extension
jupyter labextension uninstall <extension_name>
9. Logging & Debugging
If the JupyterLab server fails to start without printing a detailed error message, run it in debug mode to inspect full startup traces, configuration evaluation, and extension loading:
CLI Debug Flag
/path/to/venv/bin/jupyter lab --config /path/to/config.py --debug
Configuration-based Debug Flag
# jupyter_lab_config.py
c.Application.log_level = 'DEBUG'
Key Output Areas in Debug Logs:
- Config Searching: Shows which configuration files were discovered and loaded.
- Extension Initialization: Pinpoints which server extension threw an unhandled exception.
- Binding Events: Confirms the exact IP interface and port the server is listening on.
Production Best Practices
| Setting | Recommended |
|---|---|
ip | 0.0.0.0 (containers), 127.0.0.1 (local only) |
port | Fixed port with proxy mapping |
port_retries | 0 in production, default locally |
allow_root | Avoid if possible; use only in containers when necessary |
token | Keep enabled unless protected by external authentication |
disable_check_xsrf | Keep False unless you fully understand the implications |
root_dir | Use an absolute, existing directory |
| Configuration | Prefer ServerApp settings over legacy NotebookApp |
| Reverse Proxy | Enable HTTP/1.1 and WebSocket upgrade headers |
| Virtual Environment | Launch the jupyter executable from the correct environment |
| Logging | Use --debug when troubleshooting |
Summary
Most Jupyter configuration issues fall into one of these categories:
- Incorrect network binding (
127.0.0.1vs0.0.0.0) - Port conflicts or incorrect proxy mapping
- Authentication or XSRF misconfiguration
- Filesystem permission problems
- Invalid or non-existent
root_dir - Mixing legacy
NotebookAppand modernServerAppsettings - Wrong Python environment or broken kernels
- Missing WebSocket support in reverse proxies
- Extension compatibility issues
- TLS/HTTPS or
base_urlmisconfiguration
Understanding how these layers interact makes diagnosing Jupyter deployment issues much faster, whether running locally, in Docker, Kubernetes, or behind a production reverse proxy.
Task Scenario & Requirements
A teammate previously configured a JupyterLab server inside a Python virtual environment located at /root/code/ml-env/. However, the server was failing to function properly due to incorrect network settings and path configurations.
Here were the strict requirements for the final running state:
- Bind to all network interfaces (
0.0.0.0). - Listen on port
8888. - Set the root notebook directory to
/root/notebooks/(ensuring the directory exists on disk). - Enable root access since the server runs in a containerized environment as
root.
🔍 Step 1: Diagnosing the Misconfigurations
The initial team overrides in /root/code/jupyter_lab_config.py looked like this:
# --- xFusionCorp team overrides (review before starting the server) ---
c.IdentityProvider.token = ''
c.ServerApp.disable_check_xsrf = True
c.ServerApp.root_dir = '/root/wrong-path'
c.ServerApp.port = 8000
c.ServerApp.ip = '1.1.1.1'
What was broken?
- Invalid IP Binding (
1.1.1.1): Restricting the server to an external IP stopped local and reverse-proxy connections (like Nginx) from reaching Jupyter. - Wrong Port (
8000): The application stack expected traffic on8888. - Non-existent Directory (
/root/wrong-path): The target directory didn’t exist, and the team required/root/notebooks/. - Root Execution Blocked: Starting Jupyter as
rootwithout explicit permission caused a critical server crash, resulting in a502 Bad Gatewayfrom Nginx.
⚙️ Step 2: Applying the Fixes
1. Create the Notebooks Directory
First, make sure the target directory exists on disk:
mkdir -p /root/notebooks
2. Update jupyter_lab_config.py
Modify /root/code/jupyter_lab_config.py to fix the bindings, set the path, and explicitly allow execution as root:
# --- xFusionCorp team overrides ---
c.IdentityProvider.token = ''
c.ServerApp.disable_check_xsrf = True
c.ServerApp.root_dir = '/root/notebooks/'
c.ServerApp.port = 8888
c.ServerApp.ip = '0.0.0.0'
c.ServerApp.allow_root = True
🚀 Step 3: Starting and Verifying the Server
Launch the JupyterLab instance using the dedicated virtual environment binary:
/root/code/ml-env/bin/jupyter lab --config /root/code/jupyter_lab_config.py
Terminal Output Verification
[I 2026-07-27 06:03:31.122 ServerApp] Serving notebooks from local directory: /root/notebooks
[I 2026-07-27 06:03:31.122 ServerApp] Jupyter Server 2.20.0 is running at:
[I 2026-07-27 06:03:31.122 ServerApp] http://0.0.0.0:8888/lab
[I 2026-07-27 06:03:31.122 ServerApp] http://127.0.0.1:8888/lab
[I 2026-07-27 06:03:50.254 ServerApp] 302 GET / (@10.244.189.210) 0.43ms
Notice the successful HTTP 302 GET / request at the end — the reverse proxy connected successfully, resolving the 502 Bad Gateway error and routing straight to the interactive workspace!
💡 Key Takeaways
0.0.0.0vs Specific IPs: Always bind to0.0.0.0inside containers or VMs when you want external reverse proxies (like Nginx) or host machines to access the service.- Container Security Flags: Jupyter intentionally guards against running as root. Adding
c.ServerApp.allow_root = Truein your config file avoids execution flags in startup scripts. - Environment Isolation: Always call binaries directly from your target virtual environment (e.g.,
/root/code/ml-env/bin/jupyter) to prevent system-wide package pollution.