Skip to main content

Run Linux GUI Applications within Docker Containers

Introduction

Running GUI applications inside docker might be useful when sandbox mechanism is needed or debugging complex and tricky container networking related issues.

Generally there are 3 major ideas when trying to run GUI applications in docker, which are:

  1. Penetrate in the host X11 sockets (most straight forward but not fully secure)
  2. Expose SSH connection and use X11Forwarding configuration
  3. Run VNC server inside the container and use VNC client to access the GUI application inside

X11 Socket Penetration

X uses a client-server model to communicate. Each application is a client application that connects to a X11 socket to send UI commands to the X server and based on the DISPLAY environment variables that the application is given, it can finally show the window on the corresponding display. In order to use the display inside the container, we can forward in or penetrate the X11 socket of the host machine inside the container and share the DISPLAY environment variable so the client application in the container can talk to the host X server and therefore the window is correctly shown as expected.

There's one pitfall in such solution. X requires that the UID of the user that runs the application to align with the one that starts the X server which means that we need to add a new user in the container that has the same UID of the host user.

An example Dockerfile that starts firefox web-browser inside docker container:

FROM debian:buster

ARG uid

RUN test -n "${uid}" || (echo "docker build-arg uid must be set" && false)

RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y firefox-esr
RUN apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/bash -u ${uid} --user-group debian

USER debian
CMD /usr/bin/firefox

We need to build the container with the following command:

docker build -t firefox:debian-buster --build-arg uid=${UID} <PATH to Dockerfile>

With the docker image built, we need to run the container with X11 socket penetrated:

docker run -it --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix firefox:debian-buster

Note: if experiencing X server access control issues, we may disable these policies via the following command:

# It allows all remote access the current X server
xhost +
# or we can allow a specific IP address (i.e. 172.17.0.100) to access
xhost +inet:172.17.0.100

Note 2: The idea to improve security if needed is to run a separate X server on the host machine and use MIT-Magic-Cookie authentication for the application container.

Use SSH X11 Forwarding

Using ssh X11Forwarding actually has the worst user experiences since it splits the overall process into two phases. We need kick off a sshd server inside the container, ssh into the container and finally start application via the ssh console. As a result, it requires extra steps to do the automation.

An example Dockerfile to build a container with sshd daemon inside is shown as following:

FROM debian:buster

ARG SSH_PASSWORD

RUN test -n "${SSH_PASSWORD}" || (echo "docker build-arg SSH_PASSWORD must be set" && false)

RUN apt update
RUN apt install -y firefox-esr openssh-server xauth
RUN mkdir /var/run/sshd
RUN mkdir /root/.ssh
RUN chmod 700 /root/.ssh
RUN sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/" /etc/ssh/sshd_config
RUN sed -i "s/^.*X11Forwarding.*$/X11Forwarding yes/" /etc/ssh/sshd_config
RUN sed -i "s/^.*X11UseLocalhost.*$/X11UseLocalhost no/" /etc/ssh/sshd_config
RUN grep "^X11UseLocalhost" /etc/ssh/sshd_config || echo "X11UseLocalhost no" >> /etc/ssh/sshd_config
RUN echo "root:${SSH_PASSWORD}" | chpasswd

EXPOSE 22
ENTRYPOINT ["sh", "-c", "/usr/sbin/sshd && tail -f /dev/null"]

We need to build the container with the following command:

docker build -t firefox-ssh:debian-buster --build-arg SSH_PASSWORD=12345678 <PATH to Dockerfile>

With the docker image built, we can run the container which kicks off a sshd server inside.

docker run -it --rm -p 2222:22 firefox-ssh:debian-buster

Finally, we can connect into the container via ssh and start the application:

ssh -X root@localhost -p 2222 firefox

Run VNC Server

Using VNC server is the most thorough way when starting GUI applications. However, it might be the most resource demanding one as well. It can fully isolate the X framebuffer in the container against the host one via xvfb (X virtual framebuffer).

In order to get the best user experiences, we need to install a desktop manager. We choose xfce4 which is quite lightweight and good looking.

An example Dockerfile that installs the xfce4 and VNC server inside the container is shown as following:

FROM debian:buster

ARG uid

RUN test -n "${uid}" || (echo "docker build-arg uid must be set" && false)

RUN apt-get update
# Install VNC server and xfce4 desktop environment
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y x11vnc xvfb
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y xfce4 vim sudo
# Add fonts so applications can display correctly (Optional)
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y fonts-wqy-microhei fonts-wqy-zenhei
# Install the application
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y firefox-esr
# Clean-up apt redundant information
RUN apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/*

# Create X11 socket directory
RUN install -d -m 1777 /tmp/.X11-unix

# Create a user
RUN useradd --create-home --shell /bin/bash -u ${uid} --user-group debian --groups adm,sudo

# Change debian user's password into "debian"
RUN echo "debian:debian" | chpasswd

COPY entrypoint.sh /

USER debian
ENTRYPOINT ["sh", "/entrypoint.sh"]

EXPOSE 5900

The corresponding entrypoint.sh file is shown as following:

#!/bin/sh

if [ -z "${VNC_PASSWORD}" ]; then
    echo 'Set the VNC password via VNC_PASSWORD environment variable'
    exit -1
fi

if [ -z "${RESOLUTION}" ]; then
	echo 'Set the screen resolution via RESOLUTION environment variable'
	echo 'You might find command `xrandr` useful'
	exit -1
fi

if [ ! -f ~/.vnc/passwd ]; then
    mkdir -p ~/.vnc
    x11vnc -storepasswd "${VNC_PASSWORD}" ~/.vnc/passwd
fi

Xvfb -screen 0 "${RESOLUTION}x24" -ac &
sleep 5

export DISPLAY=:0.0
x11vnc -rfbauth ~/.vnc/passwd -noxrecord -noxfixes -noxdamage -forever -display :0 &
xfce4-session &

[ -f "/app.sh" ] && sh /app.sh || /bin/bash

We need to build the container with the following command:

docker build -t xfce4:debian-buster --build-arg uid=${UID} <PATH to Dockerfile>

With the docker image built, we can safely map the VNC port out to host machine and give the current screen resolution to run with the best user experiences.

docker run -it --rm -p 5900:5900 -e VNC_PASSWORD=12345678 -e RESOLUTION="$(xrandr | grep connected | head -n 1 | grep -o -E '[0-9]+x[0-9]+')" xfce4:debian-buster 

Using any VNC client on the host can connect to localhost:5900 to access the full desktop and any application inside the container, i.e. the installed firefox seamlessly.

Comments

Comments powered by Disqus