Run GUI Applications in a Docker Container

Docker’s normally used to containerise background applications and CLI programs. You can also use it to run graphical programs though! You can either use an existing X Server, where the host machine is already running a graphical environment, or you can run a VNC server within the container.

First it’s important to understand what Docker actually does. A Docker “container” is a form of encapsulation which seems to be superficially similar to a virtual machine. Unlike a virtual machine, containers share the same Linux kernel as their host system.

The next component is the X Window System. X Servers such as Xorg provide the fundamental graphical capabilities of Unix systems. GUI applications can’t render without an X Server available. (Alternative windowing systems, such as Wayland, are available — we’re focusing on X in this article.)

Trying to run an X Server in Docker is theoretically possible but rarely used. You’d need to run Docker in privileged mode (--privileged) so it could access your host’s hardware. Starting the server would try to claim your video devices, usually resulting in loss of video output as your host’s original X server gets its devices yanked away.

A better approach is to mount your host’s X Server socket into the Docker container. This allows your container to use the X Server you already have. GUI applications running in the container would then appear on your existing desktop.

Why Run GUI Apps in Docker?

Running a GUI program in Docker can be a useful technique when you’re evaluating a new piece of software. You can install the software in a clean container, instead of having to pollute your host with new packages.

This approach also helps you avoid any incompatibilities with other packages in your environment. If you need to temporarily run two versions of a program, you can use Docker to avoid having to remove and reinstall the software on your host.

Forwarding An X Socket to A Docker Container

Providing a Docker container with access to your host’s X socket is a straightforward procedure. The X socket can be found in /tmp/.X11-unix on your host. The contents of this directory should be mounted into a Docker volume assigned to the container. You’ll need to use the host networking mode for this to work.

You must also provide the container with a DISPLAY environment variable. This instructs X clients – your graphical programs – which X server to connect to. Set DISPLAY in the container to the value of $DISPLAY on your host.

What is X Server:

X server is a windowing system for bitmap displays, common on Linux operating systems.

The X11 server (usually Xorg these days) communicates with clients like xterm, firefox, etc via some kind of reliable stream of bytes. A Unix domain socket is probably a bit more secure than a TCP socket open to the world, and probably a bit faster, as the kernel does it all, and does not have to rely on an ethernet or wireless card.

You can encapsulate all this configuration in one docker-compose.yml file:

version: "3"services:
app:
image: my-app:latest
build: .
environment:
- DISPLAY=${DISPLAY}
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix
network_mode: host

Next, you need to create a Dockerfile for your application. Here’s an example that runs the Firefox web browser:

FROM ubuntu:latest
RUN apt-get update && apt-get install -y firefox
CMD ["/usr/bin/firefox"]

Now build and run the image:

docker-compose build
docker-compose up

A new Firefox window should appear on your desktop! The Firefox instance will run within the container, independently of any other open Firefox windows. The container will share your host’s X socket, so the containerised Firefox still shows up on your desktop.

This approach should only be used when you trust your Docker container. Exposing the host’s display server is a security risk if you’re not completely sure what lies inside the container.

Handling X Authentication

You might need to authenticate the container to access the X Server. First get an X authentication token from your host machine. Run xauth list and note down one of the listed cookies. You’ll need to copy the entire line.

Inside the Docker container, install the xauth package. Then run xauth add, passing the token you copied in the previous step.

$ apt install -y xauth 
$ xauth add <token>
$ xauth list

Your container should now successfully authenticate to the X Server.

To run a Firefox instance, simply type “firefox” inside the bash. You will find that the Firefox browser pops up on your local machine even though it is running inside the Docker Container. Using a similar process, you can run almost any user interface application inside Docker Containers and access it on your local machine.

A note about X11 risks

In the above example, the container will have full access to the host’s X server, either by disabling restrictions with xhost or by granting access through the xauth cookie. What is the risk? Well, for one, the X server must have access to draw on our screen because it creates the graphical application. This means that a malicious application running in the container (having been granted X server access) can take a screenshot of our screen whenever it wants.

But, wait, that’s not all! The X server also has access to the clipboard and input devices such as the keyboard and mouse because it needs to know how to translate movement, typing, and copy+paste actions from the host to the GUI app. So, if I run a third-party product inside my container (such as PgModeler), passing in the X11 socket can give that code the ability to log my keystrokes, manipulate my mouse movements, and read data copied into my clipboard.

X11 wasn’t designed for security, so there is no way to restrict these capabilities if you grant something access to your X server. The best we can do is limit the number of things that can access the X server. This means leaving xhost alone and passing the xauth cookie to a container whose code you trust. Don’t pass the X server socket into a random 3rd party container you have not audited!

Another Approach — Running a VNC Server

If you’re unable to use X socket forwarding, you could setup a VNC server inside your container. This approach lets you view graphical apps in the container by connecting from a VNC client running on the host.

Add the VNC server software to your container:

FROM ubuntu:latest
RUN apt-get update && apt-get install -y firefox x11vnc xvfb
RUN echo "exec firefox" > ~/.xinitrc && chmod +x ~/.xinitrc
CMD ["v11vnc", "-create", "-forever"]

When you run this container, a VNC server will be created automatically. You must bind a host port to the container’s port 5900 — this is the port the VNC server will be exposed on.

Firefox gets launched on startup as it’s added to .xinitrc. This file will be executed when the VNC server launches and initialises a new display.

To connect to the server, you’ll need a VNC client on your host. Find the IP address of your container by running docker ps, noting down the container ID and passing it to docker inspect <container>. You’ll find the IP address near the bottom of the output, within the Network node.

Use the container’s IP address with your VNC client. Connect on port 5900 without authentication. You should now be able to interact with the graphical programs running within your Docker container.

X and Docker Interaction Scenario

Desktop applications will run in Docker and will try to communicate with the X server you’re running on your PC. This can take place either with a Docker engine running on your host or in a Docker engine running on a remote machine. For X, it doesn’t really make a difference — other than some network latency being introduced.

The interaction scenario is depicted in the following figure:

X Windows and Docker interaction

X clients (your desktop applications) do not really need to know much for this communication to take place.

Actually, they don’t need to know anything but the location of the X server and an optional display that they target.

This is denoted by an environmental variable named DISPLAY, with the following syntax: DISPLAY=xserver-host:0 . The number you see after the : is the display number; for the intents and purpose of this article, we will consider this to be equivalent to “0 is the primary display attached to the X server.”

Time to run our desktop applications.

Since we’ll be running the desktop application inside a Docker container, whereas the X server will be running on the host machine, we need a way for those two to communicate.

Unfortunately, at the moment, there is no universal, out-of-the-box Docker way to do that. So keep in mind the following settings for macOS, Windows, and Linux:

macOS: -e DISPLAY=docker.for.mac.host.internal:0
Windows: -e DISPLAY=host.docker.internal:0
Linux: --net=host -e DISPLAY=:0

Eclipse IDE

Quickly fire up an IDE:

Eclipse IDE running in Docker

macOS: docker run --rm -ti -e DISPLAY=docker.for.mac.host.internal:0 psharkey/eclipse

Windows: docker run --rm -ti -e DISPLAY=host.docker.internal:0 psharkey/eclipse

Linux: docker run --rm -ti --net=host -e DISPLAY=:0 psharkey/eclipse

GDSC | IBM Z | GoogleCloudReady Facilitator | Dexterous Photographer | Quantum Computing Enthusiast | ARTH | IIEC Rise | MLOps