Introduction
The matplotlib default backend on many Linux distributions is TkAgg, which requires a graphical display server (X11 or Wayland). On headless servers -- CI runners, Docker containers, Kubernetes pods, or SSH sessions without X forwarding -- attempting to render a plot with TkAgg throws a RuntimeError because the Tk library cannot connect to a display. This error breaks automated report generation, data pipeline visualizations, and CI test suites that generate plots as artifacts.
Symptoms
Traceback (most recent call last):
File "/app/generate_report.py", line 15, in <module>
plt.savefig("output/chart.png")
File "/usr/local/lib/python3.11/site-packages/matplotlib/pyplot.py", line 1119, in savefig
res = fig.savefig(*args, **kwargs)
...
RuntimeError: Invalid DISPLAY variableOr:
_tkinter.TclError: no display name and no $DISPLAY environment variableOr with a different backend:
ImportError: Cannot load backend 'TkAgg' which requires the 'tk' interactive framework, as 'headless' backend is currently activeCommon Causes
- Default backend set to TkAgg: matplotlib's default on many Linux distros requires X11
- Missing DISPLAY environment variable: No graphical session on the server
- Docker container without X11: Container image does not include X11 libraries or virtual framebuffer
- CI/CD runner without display: GitHub Actions, GitLab CI, or Jenkins agents run headless
- Backend configured in matplotlibrc: System-wide configuration sets TkAgg as the default
- **Calling
plt.show()instead ofplt.savefig()**:plt.show()requires an interactive backend
Step-by-Step Fix
Step 1: Switch to non-interactive Agg backend
Set the backend before importing pyplot:
```python import matplotlib matplotlib.use("Agg") # Must be before pyplot import
import matplotlib.pyplot as plt
fig, ax = plt.subplots() ax.plot([1, 2, 3], [4, 5, 6]) fig.savefig("/tmp/chart.png", dpi=150, bbox_inches="tight") plt.close(fig) # Free memory ```
The Agg backend renders to PNG files without requiring a display server.
Step 2: Set backend via environment variable
Alternatively, set the MPLBACKEND environment variable:
```bash # In Dockerfile ENV MPLBACKEND=Agg
# In CI pipeline MPLBACKEND=Agg python generate_report.py
# In systemd service file [Service] Environment=MPLBACKEND=Agg ```
Or in Python code:
```python import os os.environ["MPLBACKEND"] = "Agg"
import matplotlib.pyplot as plt ```
Step 3: Configure matplotlibrc for the server
Create a system-wide or user-level configuration:
# /etc/matplotlibrc or ~/.config/matplotlib/matplotlibrc
backend: Agg
figure.dpi: 150
figure.figsize: 12, 8
savefig.bbox: tightStep 4: Use virtual framebuffer for interactive backends (if needed)
If you absolutely need an interactive backend (e.g., for testing interactive features):
```bash # Install virtual framebuffer apt-get update && apt-get install -y xvfb
# Run your script inside virtual framebuffer xvfb-run -a python generate_report.py ```
Or programmatically:
```python import os os.environ["DISPLAY"] = ":99"
import subprocess subprocess.Popen(["Xvfb", ":99", "-screen", "0", "1024x768x24"])
# Now matplotlib with TkAgg works import matplotlib.pyplot as plt ```
Prevention
- Always set
MPLBACKEND=Aggin Docker images, CI configurations, and systemd services - Use
plt.close(fig)aftersavefig()to free memory in long-running processes - Add a test in CI that imports matplotlib and checks the backend:
assert matplotlib.get_backend() == "Agg" - Include
matplotlibconfiguration in your deployment playbooks - Use
fig, ax = plt.subplots()pattern with explicit figure lifecycle management - Consider using
plotlywithkaleidoengine for static image export -- it works headless by default