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

bash
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 variable

Or:

bash
_tkinter.TclError: no display name and no $DISPLAY environment variable

Or with a different backend:

bash
ImportError: Cannot load backend 'TkAgg' which requires the 'tk' interactive framework, as 'headless' backend is currently active

Common 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 of plt.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:

ini
# /etc/matplotlibrc or ~/.config/matplotlib/matplotlibrc
backend: Agg
figure.dpi: 150
figure.figsize: 12, 8
savefig.bbox: tight

Step 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=Agg in Docker images, CI configurations, and systemd services
  • Use plt.close(fig) after savefig() 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 matplotlib configuration in your deployment playbooks
  • Use fig, ax = plt.subplots() pattern with explicit figure lifecycle management
  • Consider using plotly with kaleido engine for static image export -- it works headless by default