
Docker: launching graphical applications in containers

This is a short overview of how to run graphical applications in Docker containers.
Table of contents
- Mounting devices ( Example No. 1 )
- SSH -X ( Example No. 2 )
- Subuser ( Example No. 3 )
- Remote Desktop ( Example No. 4 )
- Related Links
Mount devices
One of the easiest ways to get a container to speak and show is to give it access to our screen and sound devices.
In order for applications in the container to be able to connect to our screen, you can use Unix domain sockets for X11, which usually lie in a directory
/tmp/.X11-unix
. Sockets can be shared by mounting this directory using the parameter -v
. You also need to set the DISPLAY environment variable , which tells applications the screen to display graphics. Since we will display on our screen, it is enough to copy the DISPLAY value of the host machine. Usually, it is :0.0
or simple :0
. An empty host name (before the colon) implies a local connection using the most efficient transport, which in most cases means Unix domain sockets, is exactly what we need:$ docker run -e DISPLAY=unix$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix ![]()
The prefix “unix” to DISPLAY is here to explicitly indicate the use of unix sockets, but more often than not this is not necessary.
Authorisation Error
At startup, you may encounter an error like:
No protocol specified Error: cannot open display: unix: 0.0
This happens when the Xsecurity extension blocks unauthorized connections to the X server. This problem can be solved, for example, by allowing all local offline connections:
$ xhost +local:
Or confine yourself to permission only for the root user:
$ xhost +si:localuser:root
In most cases this should be enough.
For those who are not looking for easy ways
Another option would be to give the container the ability to log in to the X server on its own using a pre-prepared and mounted Xauthority file. You can create one using the xauth utility , which is able to extract and export data for authorization. The catch, however, is that such an authorization record contains the name of the host on which the X server is running. Just copying it to the container is useless - the record will be ignored when trying to connect locally to the server. This problem can be solved in different ways. I will describe a couple of ways.
Hostname spoofing . The idea is simple. Retrieving an authorization entry using
Connection code spoofing . The first two bytes in each record from the Xauthority file contain the code for matching the connection family (TCP / IP, DECnet, local connections). If you assign a special FamilyWild value to this parameter (code 65535 or 0xffff), the entry will correspond to any screen and can be used for any connection (that is, the host name will not matter):
Spy on stackoverflow .
Hostname spoofing . The idea is simple. Retrieving an authorization entry using
xauth list
, change the host name to another (you need to think of it in advance) and export the resulting key to an Xauthority file, which we then mount into the container:$ DOCKER_CONTAINER_HOSTNAME=foobar
$ xauth list $DISPLAY | sed -e "s/$HOSTNAME/$DOCKER_CONTAINER_HOSTNAME/" | xargs xauth -f /tmp/.docker.Xauthority add
$ docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
-v /tmp/.docker.Xauthority -e XAUTHORITY=/tmp/.docker.Xauthority -h $DOCKER_CONTAINER_HOSTNAME ![]()
Connection code spoofing . The first two bytes in each record from the Xauthority file contain the code for matching the connection family (TCP / IP, DECnet, local connections). If you assign a special FamilyWild value to this parameter (code 65535 or 0xffff), the entry will correspond to any screen and can be used for any connection (that is, the host name will not matter):
$ xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f /tmp/.docker.Xauthority nmerge -
$ docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
-v /tmp/.docker.Xauthority -e XAUTHORITY=/tmp/.docker.Xauthority ![]()
Spy on stackoverflow .
What about the sound?
Connecting audio devices is also not difficult. In versions of Docker prior to 1.2, they can be mounted using the parameter
-v
, and the container must be run in privileged mode:$ docker run -v /dev/snd:/dev/snd --privileged ![]()
Docker 1.2 added a special option
--device
for connecting devices. Unfortunately, at the moment (version 1.2), --device
only one device at a time can be taken as a value, which means that you will have to explicitly list all of them. For instance:$ docker run --device=/dev/snd/controlC0 --device=/dev/snd/pcmC0D0p --device=/dev/snd/seq --device=/dev/snd/timer ![]()
Perhaps the function of processing all devices in the directory through
--device
will be added in future releases (there is a corresponding request on github ).Eventually
Summarizing, the command to start the container with the graphical application looks something like this:
$ docker run -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY \
--device=/dev/snd/controlC0 --device=/dev/snd/pcmC0D0p \
--device=/dev/snd/seq --device=/dev/snd/timer ![]()
Or, for Docker version below 1.2:
$ docker run -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY \
-v /dev/snd:/dev/snd --privileged ![]()
Example No. 1
I thought that to check the graphics and sound, some kind of audio player with a graphical interface would be suitable and I chose DeaDBeeF as an experimental one. To start, we do not need anything other than an image with a player installed.
Dockerfile:
FROM debian:wheezy
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -yq wget
# установка deadbeef
RUN wget -P /tmp 'http://sourceforge.net/projects/deadbeef/files/debian/0.6.2/deadbeef-static_0.6.2-2_amd64.deb' \
&& dpkg -i /tmp/deadbeef-static_0.6.2-2_amd64.deb || true \
&& apt-get install -fyq --no-install-recommends \
&& ln -s /opt/deadbeef/bin/deadbeef /usr/local/bin/deadbeef \
&& rm /tmp/deadbeef-static_0.6.2-2_amd64.deb
# будем запускать проигрыватель вместе с контейнером
ENTRYPOINT ["/opt/deadbeef/bin/deadbeef"]
Let's assemble the image:
$ docker build -t deadbeef .
Now you can start it and listen, for example, to the radio (if you decide to try it, keep in mind that the player will start at full volume):
$ docker run --rm -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY \
--device=/dev/snd/controlC0 --device=/dev/snd/pcmC0D0p --device=/dev/snd/seq --device=/dev/snd/timer \
deadbeef http://94.25.53.133/ultra-128.mp3
The result should be a working player

Ssh -x
And that’s all you need! Almost. Parameter
-X
when creating ssh compound includes redirection X11, which allows display on a local machine graphics application running on the remote. In this case, a remote machine can be understood as a docker container. In this case, an ssh server must be installed and started in the container. You should also make sure that X11 redirection is enabled in the server settings. It can be checked by looking into
/etc/ssh/sshd_config
and searching parameter X11Forwarding
(or its synonyms: ForwardX11
, AllowX11Forwarding
) which shall be set to yes
:X11Forwarding yes
What about the sound?
In ssh there is no “magic” option for redirecting sound. But it’s still possible to configure it. For example, using the PulseAudio sound server , for which you can allow client access "from the outside" (for example, from a container). The easiest way to do this is through paprefs . Having installed
paprefs
it, go to the PulseAudio settings and on the “Network Settings” tab put a checkmark in front of “Enable network access to local sound devices” (enable network access to local sound devices). There you can find the option “Don't require authentication” (do not require authorization). Enabling this option will allow unauthorized access to the server, which can simplify the configuration of docker containers. For authorized access, you must copy the file to the container ~/.pulse-cookie
.After changing the settings, PulseAudio should be restarted:
$ sudo service pulseaudio restart
or$ pulseaudio -k && pulseaudio --start
In some cases, a reboot may be required. You can check whether the settings are accepted using the command
pax11publish
, the output of which should look something like this:Server: <...>unix:/run/user/1000/pulse/native tcp::4713 tcp6::4713
Cookie: <...>
Here you can see that the audio server is listening on a unix socket (unix: / run / user / 1000 / pulse / native), 4713th TCP and TCP6 ports (standard port for PulseAudio).
In order for X applications in the container to connect to our pulse server, you need to specify its address in the PULSE_SERVER environment variable:
$ PULSE_SERVER=tcp:172.17.42.1:4713
Here "172.17.42.1" is the address of my docker host. You can find out this address, for example, like this:
$ ip route | awk '/docker/ { print $NF }'
172.17.42.1
That is, the line "tcp: 172.17.42.1: 4713" says that you can connect to the pulse server by the IP address 172.17.42.1, where it listens on TCP port 4713.
In general, such a setting is enough. I only note that when using the above method, all sound will be transmitted in clear text (unencrypted), which in the case of using the container on the local computer does not really matter. But if desired, this traffic can also be encrypted. To do this, PULSE_SERVER should be configured on any free port on localhost (for example: “tcp: localhost: 64713”). As a result, the audio stream will go to the local port 64713, which in turn can be forwarded to the 4713th port of the host machine using
ssh -R
:$ ssh -X -R 64713:localhost:4713 @
Audio data will be transmitted over an encrypted channel.
Example No. 2
As in the previous example, we describe the image of the DeaDBeF player with an ssh server. I will proceed from the fact that all the PulseAudio presets described above have been completed, which means that we can only start creating a docker image.
The player installation code will repeat the code from the example earlier. We just need to add the installation of openssh-server.
This time besides Dockerfile we will create one more file - entrypoint.sh. This is a script for the "entry point", i.e. it will be executed every time the container starts. In it, we will create a user with a random password, set the PULSE_SERVER environment variable and start the ssh server.
Dockerfile:
FROM debian:wheezy
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -yq wget
# установка deadbeef
RUN wget -P /tmp 'http://sourceforge.net/projects/deadbeef/files/debian/0.6.2/deadbeef-static_0.6.2-2_amd64.deb' \
&& dpkg -i /tmp/deadbeef-static_0.6.2-2_amd64.deb || true \
&& apt-get install -fyq --no-install-recommends \
&& ln -s /opt/deadbeef/bin/deadbeef /usr/local/bin/deadbeef \
&& rm /tmp/deadbeef-static_0.6.2-2_amd64.deb
# минимальная установка pulseaudio
RUN apt-get install -yq --no-install-recommends pulseaudio
RUN apt-get install -yq \
pwgen \
openssh-server
# создаем директорию необходимую для запуска ssh-сервера
RUN mkdir -p /var/run/sshd
ADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 22
ENTRYPOINT ["/entrypoint.sh"]
We will pass the port and address of the pulse server as parameters when the container starts, in order to be able to change them to non-standard ones (without forgetting the default values).
entrypoint.sh:
#!/bin/bash
# формируем переменную окружения PULSE_SERVER
PA_PORT=${PA_PORT:-4713}
PA_HOST=${PA_HOST:-localhost}
PA_SERVER="tcp:$PA_HOST:$PA_PORT"
# имя пользователя для подключения
DOCKER_USER=dockerx
# генерируем пароль пользователя
DOCKER_PASSWORD=$(pwgen -c -n -1 12)
DOCKER_ENCRYPTED_PASSWORD=$(perl -e 'print crypt('"$DOCKER_PASSWORD"', "aa")')
# выводим имя и пароль пользователя,
# чтобы их можно было увидеть с помощью docker logs
echo User: $DOCKER_USER
echo Password: $DOCKER_PASSWORD
# создаем пользователя
useradd --create-home --home-dir /home/$DOCKER_USER --password $DOCKER_ENCRYPTED_PASSWORD \
--shell /bin/bash --user-group $DOCKER_USER
# добавляем инициализацию и экспорт PULSE_SERVER в ~/.profile для нового пользователя,
# чтобы она была доступна во время его сессии
echo "PULSE_SERVER=$PA_SERVER; export PULSE_SERVER" >> /home/$DOCKER_USER/.profile
# запускаем ssh-сервер
exec /usr/sbin/sshd -D
Let's assemble the image:
$ docker build -t deadbeef:ssh .
Let's start the container, not forgetting to specify the PulseAudio host (I will omit the port, since I have it standard), and call it “dead_player”:
$ docker run -d -p 2222:22 -e PA_HOST="172.17.42.1" --name=dead_player deadbeef:ssh
To find out the user password for connection, use the command
docker logs
:$ docker logs dead_player
User: dockerx
Password: vai0ay7OuNga
To connect via ssh, you can use the address of both the container itself and the address of the docker host (in this case, the connection port will be different from the standard 22nd; in this case, it will be 2222 - the one we specified when starting the container). You can find out the IP of the container using the command
docker inspect
:$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' dead_player
172.17.0.69
Connection to the container:
$ ssh -X dockerx@172.17.0.69
Or through the docker gateway:
$ ssh -X -p 2222 dockerx@172.17.42.1
Finally, after authorization, you can relax and listen to music:
dockerx@5e3add235060:~$ deadbeef
Subuser
Subuser allows you to run programs in isolated docker containers, taking care of all the work involved in creating and configuring containers, so even people who don’t know anything about docker can use it. In any case, this is the idea of the project. For each container application, restrictions are set depending on its purpose - limited access to host directories, network, sound devices, etc. In fact, subuser implements a convenient wrapper over the first way to launch graphical applications described here, since the launch is carried out by mounting the necessary directories, devices, etc.
Each created image is accompanied by a permissions.json file that defines access settings for the application. So, for example, this file looks for a vim image:
{
"description" : "Simple universal text editor."
,"maintainer" : "Timothy Hobbs "
// Путь к исполняемому файлу внутри контейнера
,"executable" : "/usr/bin/vim"
// Список директорий, к которым программа должна иметь доступ с правами на чтение/запись
// Пути задаются относительно вашей домашней директории. Например: "Downloads" преобразуется в "$HOME/Downloads"
,"user-dirs" : [ 'Downloads', 'Documents' ] // По умолчанию: []
// Разрешить программе создавать X11 окна
,"x11" : true // По умолчанию: false
// Разрешить программе использовать вашу звуковую карту и устройства записи
,"sound-card" : true // По умолчанию: false
// Дать программе доступ к директории, из которой она была запущена с правами на чтение/запись
,"access-working-directory" : true // По умолчанию: false
// Дать программе доступ к интернету
,"allow-network-access" : true // По умолчанию: false
}
Subuser has a repository (currently small) of ready-made applications, a list of which can be seen using the command:
$ subuser list available
You can add an application from the repository as follows:
$ subuser subuser add firefox-flash firefox-flash@default
This command installs the application, calling it firefox-flash, based on the image of the same name from the default repository.
The launch of the application looks like this:
$ subuser run firefox-flash
In addition to using the standard repository, you can create and add your own subuser applications.
The project is quite young and still looks damp, but it performs its task. Project code can be found on github: subuser-security / subuser
Example No. 3
Let's create a subuser application for the same DeaDBeef. To demonstrate, create a local subuser repository (nothing more than a git repository). The directory
~/.subuser-repo
will go down. It should initialize the git repository:$ mkdir ~/.subuser-repo
$ cd ~/.subuser-repo
$ git init
Here we create a directory for DeaDBeef:
$ mkdir deadbeef
The directory structure for the subuser image is as follows:
image-name/
docker-image/
SubuserImagefile
docker-build-context...
permissions.json
SubuserImagefile is the same Dockerfile. The only difference is the ability to use an instruction
FROM-SUBUSER-IMAGE
that takes the identifier of an existing subuser image as an argument. A list of available base images can be found here: SubuserBaseImages . So, having prepared the directory structure, it remains to create two files: SubuserImagefile and permissions.json.
SubuserImagefile will be practically no different from the Dockerfile given earlier:
FROM debian:wheezy
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -yq wget
# установка deadbeef
RUN wget -P /tmp 'http://sourceforge.net/projects/deadbeef/files/debian/0.6.2/deadbeef-static_0.6.2-2_amd64.deb' \
&& dpkg -i /tmp/deadbeef-static_0.6.2-2_amd64.deb || true \
&& apt-get install -fyq --no-install-recommends \
&& ln -s /opt/deadbeef/bin/deadbeef /usr/local/bin/deadbeef \
&& rm /tmp/deadbeef-static_0.6.2-2_amd64.deb
In permissions.json, we describe the access settings for our player. We need a screen, sound and internet:
{
"description": "Ultimate Music Player For GNU/Linux",
"maintainer": "Humble Me",
"executable": "/opt/deadbeef/bin/deadbeef",
"sound-card": true,
"x11": true,
"user-dirs": [
"Music"
],
"allow-network-access": true,
"as-root": true
}
The parameter
as-root
allows running applications in the container as root. By default, subuser starts a container with a parameter --user
, assigning it the identifier of the current user. But deadbeef at the same time refuses to start (cannot create a socket file in a home directory that does not exist). Let's commit the changes in our impromptu subuser repository:
~/.subuser-repo $ git add . && git commit -m 'initial commit'
Now you can install the player from the local repository:
$ subuser subuser add deadbeef deadbeef@file:////home/silentvick/.subuser-repo
Finally, you can start it with the following command:
$ subuser run deadbeef
Remote Desktop
Since the container is so similar to a virtual machine, and the interaction with it resembles a network one, the solution immediately comes to mind in the form of remote access systems, such as: TightVNC , Xpra , X2Go , etc.
This option looks more overhead, since it requires the installation of additional software - both in the container and on the local computer. But it has advantages, for example:
- high isolation of the container, which increases security when running suspicious programs
- traffic encryption
- network data optimization
- multi-platform
- as well as the ability to launch a full graphical environment
Example No. 4
As an example, I will use X2Go, because Of the solutions I tried, he liked his ease of use, as well as built-in support for broadcasting sound. A special program is used to connect to the x2go server
x2goclient
. More information about installing the server and client can be found on the official website of the project. As for what we will install, you can, of course, install a full-fledged graphical shell into the container like LXDE, XFCE, or even Gnome with KDE. But it seemed to me unnecessary for the conditions of this example. Enough of us and OpenBox .
In the container, in addition to the x2go server, you will also need an ssh server. Therefore, the code will be much similar to that shown in example No. 2. In the part where the player is installed, the openssh server, and the pulseaudio server. That is, it remains only to add the x2go server and openbox:
Dockerfile:
FROM debian:wheezy
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get install -yq wget
# установка deadbeef
RUN wget -P /tmp 'http://sourceforge.net/projects/deadbeef/files/debian/0.6.2/deadbeef-static_0.6.2-2_amd64.deb' \
&& dpkg -i /tmp/deadbeef-static_0.6.2-2_amd64.deb || true \
&& apt-get install -fyq --no-install-recommends \
&& ln -s /opt/deadbeef/bin/deadbeef /usr/local/bin/deadbeef \
&& rm /tmp/deadbeef-static_0.6.2-2_amd64.deb
# минимальная установка pulseaudio
RUN apt-get install -yq --no-install-recommends pulseaudio
# устанавливаем ssh-сервер и утилиту pwgen для генерации пароля
RUN apt-get install -yq \
pwgen \
openssh-server
# создаем директорию, необходимую для запуска ssh-сервера
RUN mkdir -p /var/run/sshd
# установка x2go-сервера
RUN echo 'deb http://packages.x2go.org/debian wheezy main' >> /etc/apt/sources.list \
&& echo 'deb-src http://packages.x2go.org/debian wheezy main' >> /etc/apt/sources.list \
&& apt-key adv --recv-keys --keyserver keys.gnupg.net E1F958385BFE2B6E \
&& apt-get update && apt-get install -yq x2go-keyring \
&& apt-get update && apt-get install -yq \
x2goserver \
x2goserver-xsession
# установка openbox
RUN apt-get install -yq openbox
# Добавляем пункт запуска DeaDBeeF в меню OpenBox.
#
# Для демонстрации сгодится и такой прямолинейный метод,
# но, в общем случае, лучше создать свой файл menu.xml и добавлять его через ADD
RUN sed -i '/<.*id="root-menu".*>/a deadbeef ' \
/etc/xdg/openbox/menu.xml
ADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 22
ENTRYPOINT ["/entrypoint.sh"]
We’ll also modify the entrypoint.sh script a bit. We no longer need to set the PULSE_SERVER environment variable, so you can get rid of this code. In addition, the user to connect should be added to the x2gouser group, otherwise he will not be able to start the x2go session:
#!/bin/bash
# имя пользователя для подключения
DOCKER_USER=dockerx
X2GO_GROUP=x2gouser
# генерируем пароль пользователя
DOCKER_PASSWORD=$(pwgen -c -n -1 12)
DOCKER_ENCRYPTED_PASSWORD=$(perl -e 'print crypt('"$DOCKER_PASSWORD"', "aa")')
# выводим имя и пароль пользователя,
# чтобы их можно было увидеть с помощью docker logs
echo User: $DOCKER_USER
echo Password: $DOCKER_PASSWORD
# создаем пользователя
useradd --create-home --home-dir /home/$DOCKER_USER --password $DOCKER_ENCRYPTED_PASSWORD \
--shell /bin/bash --groups $X2GO_GROUP --user-group $DOCKER_USER
# запускаем ssh-сервер
exec /usr/sbin/sshd -D
Let's assemble the image:
$ docker build -t deadbeef:x2go .
Run the container in daemon mode:
$ docker run -d -p 2222:22 --name=dead_player deadbeef:x2go
Now that the container is working, you can connect to it using x2goclient, as we would connect to any remote machine. In the connection settings, either the container itself or the docker host should be specified as the host (in this case, it is also worth considering the non-standard ssh connection port). You can find out the login and password for authorization using the command
docker logs
, as shown in example No. 2. To start an openbox session in the “Session type” settings, select “Custom desktop” and write “openbox-session” in the “Command” field. After connecting, you can start the player through the openbox menu (right-click) and check its performance:
screenshot

If desired, you can achieve a more neat look:
screenshot

But this is a completely different story.
I also add that X2Go allows you to run single applications, as if they were running on the local machine. To do this, select “Single application” in the “Session type” settings in the client and specify the path to the executable file in “Command”. In this case, the container does not even need to install a graphical environment - it is enough to have an X2Go server and the desired application.
Related Links
- Desktop integration examples - a couple of examples of images to run by mounting devices from the official Docker repository
- jlund / docker-chrome-pulseaudio - Google Chrome image using pulseaudio to transmit sound
- tomparys / skype - skype in the container (pulseaudio is used for sound)
- dockerfile / ubuntu-desktop - an image of Ubuntu LXDE with a VNC server on board
- rogaha / docker-desktop - Ubuntu + FluxBox using Xpra
- paimpozhil / DockerX2go - multiple images with different OSs using X2go
- thewtex / docker-opengl-nvidia - an image that allows you to run applications using proprietary Nvidia drivers with OpenGL support
- Can you run GUI apps in a docker container? - discussion of the issue on stackoverflow