Making Matplotlib in Docker draws in Windows through WSL

A not so simple Docker example

The initial title of this post is "A Simple Docker Example", which is going to show how to plot a simple Python program into a Docker image and run it. But it turns out that I’ve chosen a not so simple one as an example.

Few days ago, I have annotated several hundred of bib images with their corresponding bib numbers for my Bib-Racer-Recognition project. I want to double check whether I have tagged the bib numbers correctly for the images, so I just created a simple program to show the original images and the corresponding bib numbers. It proved that this step is necessary as the program did help me spot some wrongly tagged images.

My PC is running Windows 10 and the Docker image is built on top of the official Python linux base image, so it makes sense to run the Linux container in Windows 10’s WSL (Windows Subsystem for Linux). This can let the container and the host talk in same language and let the WSL to handle the actual display issue with Windows. The steps include setting up WSL in Windows 10, prepare the Docker image, and correctly start and run the container.

WSL in Windows 10

  1. Install or upgrade to WSL 2

  2. Make pyplot displays figure in WSL

    1. Download and install VCXsrv

    2. export DISPLAY=`grep -oP "(?<=nameserver ).+" /etc/resolv.conf`:0.0

      The above command is to extract the IP address of the nameserver and export to the DISPLAY environment variable. So you can cat /etc/resolv.conf to get the IP address and then export DISPLAY=<IP address>:0.0.

    3. Press Windows key and type “Allow an app through Windows Firewall”, check VcXsrv can talk through the Firewall for both public and private network.

      Both Private and Public are checked for VcXsrv in Windows Defender Firewall

    4. Launch VcXsrv with “Disable access control” ticked.

      Enable “Disable access control”

    Please refer to the original post for more details and discussion.

Image preparation

Since the program is just to show the images and the corresponding bib numbers, the only things need to be prepare are the images, the Python script file and the Dockerfile, as listed below:

.
├── Dockerfile
├── images
│   ├── 00001.jpg
│   ├── 00002.jpg
│   ├── 00003.jpg
│   ├── 00004.jpg
│   ├──
│   ├── ...
│   ├──
│   ├── 00576.jpg
│   ├── 00577.jpg
│   ├── 00578.jpg
│   └── bib_numbers.txt
└── verify_test_bib_numbers.py

Therefore the Dockerfile is very simple.

FROM python:latest
RUN pip install matplotlib
WORKDIR /app
COPY verify_test_bib_numbers.py ./
CMD ["python", "./verify_test_bib_numbers.py", "./images/"]

We can then build the image by running: docker build -t bib_verify .

Steps to start the container

This is the part that I spent most of my time, in researching the way to properly set the environment for the container to run the programs. The point is pyplot, thus matplotlib in the container needs to be able to access the X11 socket of the host, in turns the X11 in WSL needs to utilize the GUI in Windows. The second part has already been set in the above section. This section is aiming at the first part.

In order to enable remote user can access the X server in the host (WSL), we should grant access control by using xhosts:

  1. sudo apt install x11-xserver-utils

  2. xhost +

    Certainly, we can fine tune which should be allowed by xhost instead of allowing all the connections. There are more discussion here: docker _tkinter.TclError: couldn’t connect to display

When starting to run the container, we need to specify the Unix-domain socket for container to communicate with the host by mounting the path in container /tmp/.X11-unix to the same directory in the host, and also specify the DISPLAY environment variable. And hence is the below command to start a container:

docker run -it \
  --mount type=bind,source="$(pwd)"/images,target=/app/images \
  -v "/tmp/.X11-unix:/tmp/.X11-unix:rw" \
  -e DISPLAY=$DISPLAY bib_verify
Running the Bib verification application in a Docker container in WSL

Settings in matplotlib

There are many discussion on setting the ‘backend’ in matplotlib, e.g. here and here, for rendering in different environments, such as saving to a file or rendering to a GUI. Basically, there are two common methods to do this:

  • Edit MATPLOTLIBRC:

    1. Check which matplotlibrc is used: matplotlib.matplotlib_fname(). Then set environment variable MATPLOTLIBRC to the path to that file, or place it at the same directory of your script file.
    2. Set backend in matplotlibrc, e.g. backend: TkAgg

    Or,

  • Put maplotlib.use('TkAgg') at the beginning of your script.

However, I found these steps may not be necessary. If we take a look at matplotlibrc, it is written as:

## The default backend. If you omit this parameter, the first
## working backend from the following list is used:
## MacOSX Qt5Agg Qt4Agg Gtk3Agg TkAgg WxAgg Agg

At least in my case, TkAgg is automatically selected even the above steps have not been taken. Certainly you can also set it manually if necesary.

Conclusion

This not so simple Docker example did let me spend more than a half day effort to sort out the matplotlib rendering in Docker problem. It’s challenging but interesting at the same time. Source code can be found here and comments are welcome. Ciao!

Avatar
Leo Mak
Make the world a better place, piece by piece.
comments powered by Disqus

Related