
A fresh Ubuntu 26.04 box looks like the easy case: Python is already there, pip is a command away, and the install should take thirty seconds. Then pip install plotly answers with an “externally-managed-environment” error. Or the install works, but fig.write_image() fails because Kaleido can’t find Chrome. Or the notebook renders a blank cell behind a reverse proxy. None of this is a Plotly bug. It comes from how modern Ubuntu treats system Python, and from how Plotly splits its work between a pure-Python core and a browser-based rendering layer.
This guide covers the install methods that hold up on a real server. It starts with the one that works for most people (a virtual environment), then covers apt, Jupyter, Dash behind Nginx, static image export, Docker and troubleshooting. Where it matters, the guide explains why each step exists, because the next release will shift something and understanding beats memorizing.
Ubuntu 26.04 LTS, codenamed Resolute Raccoon, shipped in April 2026. It ships Python 3.14 in the default repositories. That is a fairly new interpreter for most scientific packages, and it shapes several decisions below.
What You Are Actually Installing
Plotly is not one thing. The plotly package on PyPI is the Python graphing library. It supports over 40 chart types across statistical, financial, geographic, scientific and 3D use cases. Under it sit a few optional pieces that people often confuse.
- plotly (core): builds figure objects and renders them as interactive HTML using plotly.js, which ships inside the package.
- Plotly Express: the high-level API (
px.line,px.scatter). It needs a dataframe library, usually pandas, plus NumPy. - Kaleido: the static image exporter for PNG, SVG and PDF. Version 1.0.0 or newer is required, and it needs Chrome or Chromium to render images.
- Dash: a separate framework for building web apps on top of Plotly. It is not part of the core install.
- JupyterLab or notebook: where most people see Plotly charts first. It is optional, and it needs a little configuration.
Knowing this split tells you where a failure comes from. A chart that doesn’t draw is a front-end problem. A chart that won’t export is a Kaleido or Chrome problem. An install that won’t complete is a Python or pip problem.
Prerequisites and Pre-Flight Checks
Before touching anything, confirm what the machine has. Five seconds of checking saves twenty minutes of guessing.
lsb_release -a
python3 --version
which python3
free -h
df -h /
You should see Ubuntu 26.04 and a Python 3.14.x interpreter. One reference install on 26.04 reported Python 3.14.3. Point releases move, so don’t worry if yours differs slightly.
Update the package index and install the base tooling first:
sudo apt update
sudo apt install -y python3 python3-venv python3-pip python3-dev build-essential
Each package earns its place:
python3-venvprovides thevenvmodule. On Debian-family systems it is split out from the main Python package, and skipping it leads to the well-known “ensurepip is not available” error.python3-pipgives you pip for the system interpreter. You will mostly use it from inside a venv.python3-devandbuild-essentialare insurance. When a wheel isn’t available for your Python version, pip falls back to compiling from source, and that needs headers and a compiler.
That last point matters more on 3.14 than it did on older releases. Compiled dependencies sometimes lag a brand-new interpreter. One Kaleido issue report shows pip install plotly[kaleido] failing because orjson had to compile from source on the user’s machine. Having the toolchain installed turns that failure into a slow install instead of a dead end.
Method 1: Install Plotly in a Virtual Environment (Recommended)
This is the method to use on a production server, a workstation, or anything you intend to keep. It isolates Plotly and its dependencies from the operating system’s own Python packages. Several system tools, including parts of apt’s tooling, depend on system Python. Breaking them with a careless sudo pip install is a classic way to ruin an afternoon.
Step 1: Create a project directory and environment
mkdir -p ~/projects/plotly-lab
cd ~/projects/plotly-lab
python3 -m venv .venv
source .venv/bin/activate
The prompt now shows (.venv). From here, python and pip refer to the isolated copy.
Step 2: Upgrade pip and install Plotly
python -m pip install --upgrade pip
python -m pip install plotly
Using python -m pip instead of bare pip is a habit worth building. It guarantees pip belongs to the interpreter you are looking at. Mismatched pip and python binaries cause a large share of “I installed it but can’t import it” reports.
Step 3: Add the extras you will probably need
python -m pip install "plotly[express]" pandas numpy
The official docs describe the plotly[express] extra as the way to pull in Plotly Express dependencies, and note that you also need a supported dataframe library. Installing pandas explicitly keeps things predictable.
Step 4: Verify the install
python - <<'EOF'
import plotly
print("Plotly version:", plotly.__version__)
import plotly.express as px
fig = px.line(x=[1, 2, 3, 4], y=[3, 1, 4, 2], title="Smoke test")
fig.write_html("smoke_test.html")
print("HTML written OK")
EOF
If you see a version string and “HTML written OK”, the core works. Open smoke_test.html in a browser (copy it to your desktop if the server is headless) and confirm the chart is interactive. Hovering should show tooltips, and the toolbar should appear at the top right.
Pin your dependencies
Reproducibility is what separates a lab experiment from a deployable project:
python -m pip freeze > requirements.txt
On a new server, recreate the environment with python -m pip install -r requirements.txt. For stricter control, tools like pip-tools or uv can generate fully pinned lock files. Many admins now use uv for its install speed. It is a reasonable choice, but plain venv plus pip is the baseline and works everywhere.
Method 2: Install Plotly with apt
Ubuntu packages Plotly as python3-plotly, so you can install it system-wide:
sudo apt update
sudo apt install -y python3-plotly
It is clean, signed by the distribution, and updated through normal security channels. The tradeoff is version age. Distribution packages freeze at whatever was current when the release was assembled. For reference, a package tracker showed python3-plotly at 5.20.0 for Ubuntu 24.10. Check what 26.04 actually offers before you commit:
apt-cache policy python3-plotly
When is apt the right choice? Mostly when another system package or a corporate image requires it, or when you need a locked, audited, no-internet install. If you are following current Plotly documentation, the pip route in a venv gives you features that the distro version may not have. Mixing both in one environment is where things get confusing, so pick one per project.
Method 3: The Externally-Managed-Environment Error
Anyone who has tried pip install plotly outside a venv on recent Ubuntu has met this:
error: externally-managed-environment
× This environment is externally managed
This is deliberate. Debian and Ubuntu mark the system Python as managed by apt (PEP 668). Pip refuses to write into it, because mixing pip and apt packages in the same directory can leave the OS in an inconsistent state.
Your options, from best to worst:
- Use a virtual environment (Method 1). This is the right answer almost every time.
- Use
pipxfor command-line tools:sudo apt install pipx, thenpipx install <tool>. This is not for libraries you import in your own code. - Use
apt install python3-plotlyfor a system-level library. - Use
--break-system-packagesonly on a disposable container or VM. On a server you care about, don’t.
Skip option 4. It exists for a reason, and the reason is that you may regret using it.
Installing JupyterLab with Plotly
Interactive exploration is where Plotly shines, and JupyterLab is the usual front end. Inside the same venv:
python -m pip install jupyterlab
jupyter lab --no-browser --ip=127.0.0.1 --port=8888
Modern Plotly (v6 and later) renders in JupyterLab through anywidget, so the old labextension build steps from past tutorials are mostly obsolete. If you follow an older guide that tells you to run jupyter labextension install jupyterlab-plotly, ignore it. That advice predates the current rendering approach.
Test with a cell like this:
import plotly.express as px
df = px.data.gapminder().query("year == 2007")
fig = px.scatter(df, x="gdpPercap", y="lifeExp", size="pop",
color="continent", hover_name="country", log_x=True)
fig.show()
The sample dataset needs pandas, which you already installed.
Accessing Jupyter on a remote server safely
Binding Jupyter to 0.0.0.0 on a public IP is a bad idea. Bots find open notebook servers quickly, and a notebook is remote code execution by design. Use an SSH tunnel instead:
ssh -N -L 8888:127.0.0.1:8888 user@your-server-ip
Then browse to http://127.0.0.1:8888 on your own machine. The token printed in the server log authenticates you. It takes ten seconds to set up and removes a whole class of exposure.
Static Image Export with Kaleido and Chrome
Interactive HTML is great. Reports, PDFs, emails and automated dashboards need static images. That requires Kaleido.
python -m pip install --upgrade kaleido
The Plotly docs state that static export needs Kaleido 1.0.0 or newer, and that Kaleido needs Chrome or Chromium. It uses the installation already on the system, or you can fetch one with plotly_get_chrome.
On a server with no browser:
plotly_get_chrome
Then test it:
import plotly.express as px
fig = px.bar(x=["A", "B", "C"], y=[5, 3, 7])
fig.write_image("bar.png", width=900, height=500, scale=2)
fig.write_image("bar.svg")
fig.write_image("bar.pdf")
Why Chrome fails on a headless server
Kaleido drives a real browser engine. On minimal server images, Chrome may start but exit because shared libraries are missing. Install the usual runtime libraries:
sudo apt install -y libnss3 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 \
libgbm1 libpango-1.0-0 libasound2t64
Ubuntu renamed several libraries with a t64 suffix during the 64-bit time_t transition. If apt says a package has no installation candidate, try the name with or without t64, and search with apt-cache search libasound. Old copy-paste lists from 22.04 tutorials will often fail for exactly this reason.
If you run Kaleido as a service user inside containers or under strict sandboxing, Chrome’s sandbox can also fail to start. Do not disable the sandbox on a server that processes untrusted data. Fix user namespace permissions or run the container with the proper security profile instead.
Older Kaleido 0.2.x bundled its own renderer and did not need Chrome. If you are maintaining legacy code pinned to Kaleido 0.2.1, it still installs from PyPI with prebuilt wheels for common platforms. For anything new, use the current version and plan for the Chrome dependency.
Running Dash Apps in Production
Plotly charts often end up inside Dash apps. A quick setup:
python -m pip install dash gunicorn
A minimal app.py:
from dash import Dash, dcc, html
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")
app = Dash(__name__)
server = app.server
app.layout = html.Div([html.H3("Iris"), dcc.Graph(figure=fig)])
if __name__ == "__main__":
app.run(debug=False)
Never run debug=True on a public server. The debugger exposes an interactive console.
For production, use Gunicorn rather than the built-in development server:
gunicorn app:server --workers 3 --bind 127.0.0.1:8050 --timeout 60
A systemd unit keeps it alive across reboots:
# /etc/systemd/system/dashapp.service
[Unit]
Description=Dash app via Gunicorn
After=network.target
[Service]
User=dashsvc
Group=dashsvc
WorkingDirectory=/opt/dashapp
ExecStart=/opt/dashapp/.venv/bin/gunicorn app:server --workers 3 --bind 127.0.0.1:8050
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now dashapp
sudo systemctl status dashapp
Then put Nginx in front for TLS and compression:
server {
listen 80;
server_name charts.example.com;
location / {
proxy_pass http://127.0.0.1:8050;
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;
proxy_read_timeout 90;
}
gzip on;
gzip_types application/json application/javascript text/css;
}
Test with sudo nginx -t, reload with sudo systemctl reload nginx, and issue a certificate with Certbot. Compression matters here. Plotly figures serialize to JSON, and large figures compress very well, often by 80 percent or more. On a slow mobile connection that is the gap between a snappy dashboard and a spinner.
Docker Install for Reproducible Builds
If you want the same environment on a laptop, a CI runner and a server, containerize it:
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nobody
CMD ["gunicorn", "app:server", "--bind", "0.0.0.0:8050", "--workers", "2"]
Build and run:
docker build -t plotly-app .
docker run -d --name plotly-app -p 127.0.0.1:8050:8050 plotly-app
Binding the published port to 127.0.0.1 keeps it behind your reverse proxy. A common mistake is publishing -p 8050:8050, which on many setups bypasses host firewall rules, because Docker writes its own iptables entries. If you need Kaleido image export in the container, add Chrome and its libraries to the image, or generate images in a separate worker container so the web tier stays lean.
Troubleshooting Common Errors
ModuleNotFoundError: No module named ‘plotly’
Almost always an environment mismatch. Check which interpreter runs:
which python
python -c "import sys; print(sys.executable)"
python -m pip show plotly
If pip show finds it but your script does not, you are launching the script with a different Python, often a systemd unit or cron job that does not activate the venv. Call the venv interpreter by absolute path: /opt/dashapp/.venv/bin/python script.py.
error: externally-managed-environment
Use a venv, as described above.
The virtual environment was not created successfully because ensurepip is not available
Install the missing package: sudo apt install python3-venv. Then delete the half-created folder with rm -rf .venv and retry.
Build failures during pip install (orjson, numpy, others)
Check whether the failure mentions compiling from source. This happens when no wheel exists for your Python version and platform yet. One reported case with Python 3.14 showed orjson failing to build while installing Kaleido extras. Fixes, in order:
- Upgrade pip:
python -m pip install --upgrade pip. - Install the toolchain:
sudo apt install build-essential python3-dev. Some packages also need Rust, available withsudo apt install rustc cargo. - Retry later or pin to a release that publishes wheels for your interpreter.
- As a last resort, install an older Python in a separate environment (for example with
uv python install) when a critical dependency has not caught up.
ValueError: Image export using the “kaleido” engine requires the Kaleido package
Install or upgrade it: python -m pip install --upgrade kaleido. Then confirm Chrome is available, or run plotly_get_chrome. Version mismatches between an old Plotly and new Kaleido can also trigger this, so upgrade both together.
Kaleido hangs or times out
Usually Chrome cannot launch. Run the browser by hand to see the error (google-chrome --headless --no-sandbox --dump-dom about:blank, for diagnosis only) and install missing libraries with ldd hints. Low-memory servers can also kill Chrome. Check dmesg | grep -i oom.
Blank figure in Jupyter
Restart the kernel and confirm the notebook server and kernel use the same environment. Try fig.show(renderer="browser") to rule out front-end problems. Behind a proxy, make sure WebSocket upgrade headers are passed.
Dash app returns 502 behind Nginx
Check that Gunicorn is running (systemctl status dashapp), that it is bound to the same address Nginx proxies to, and read the journal with journalctl -u dashapp -n 100 --no-pager. SELinux does not apply on Ubuntu by default, but on AlmaLinux you may need setsebool -P httpd_can_network_connect 1.