📜 ⬆️ ⬇️

Docker: running graphical applications in containers

Strictly speaking, Docker was not created for this kind of things, namely the launch of graphical applications. However, from time to time there are questions in Docker topics about whether it is possible to launch a GUI application in a container. The reasons may be different, but more often than not, this desire to change an unnecessarily bulky virtual machine to something easier, without losing in convenience and retaining a sufficient level of isolation.

This is a brief overview of how to launch graphical applications in Docker containers.

Table of contents



Mounting devices


One of the easiest ways to get a container to talk and show is to give it access to our screen and sound devices.

In order for applications in the container to connect to our screen, you can use Unix domain sockets for X11, which are usually located in the /tmp/.X11-unix directory. Sockets can be shared by mounting this directory with the -v . You also need to set the DISPLAY environment variable, which gives applications a screen for displaying graphics. Since we will be outputting to our screen, it is enough to copy the host machine's DISPLAY value. Usually, this is :0.0 or simply :0 . An empty hostname (before the colon) implies a local connection using the most efficient transport, which in most cases means Unix-domain sockets - just what we need:
')
 $ docker run -e DISPLAY=unix$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix <image> 

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 of the form:

 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 non-network connections:

 $ xhost +local: 

Or restrict permission to root-user only:

 $ 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 enable the container to independently authenticate on the X-server 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 snag, however, is that such an authorization record contains the name of the host on which the X server is running. Just copying it into a container is useless - the record will be ignored when trying to connect to the server locally. This problem can be solved in different ways. I will describe a couple of ways.

The substitution of the host name . The idea is simple. Extract the authorization entry using the xauth list , change the host name to another (you need to think up in advance) and export the resulting key to the 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 <image> 

Connection code substitution . The first two bytes in each entry from the Xauthority file contain the connection family matching code (TCP / IP, DECnet, local connections). If you assign the FamilyWild special value (code 65535 or 0xffff) to this parameter, the entry will correspond to any screen and can be used for any connection (that is, the host name will not have a value):
 $ 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 <image> 

Peeped on stackoverflow .

And what about the sound?


Connecting sound devices is also not a big deal. In Docker versions up to 1.2, they can be mounted using the -v parameter, while the container should be running in privileged mode:

 $ docker run -v /dev/snd:/dev/snd --privileged <image> 

A special option --device for connecting devices has been added to Docker 1.2. Unfortunately, at the moment (version 1.2), --device as the value can take only one device at a time, which means you have to explicitly list them all. For example:

 $ docker run --device=/dev/snd/controlC0 --device=/dev/snd/pcmC0D0p --device=/dev/snd/seq --device=/dev/snd/timer <image> 

Perhaps the function of processing all devices in the directory via --device will be added in future releases (there is a corresponding request on github).

Eventually


To summarize, the command to launch a container with a graphics application looks 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 <image> 

Or, for Docker versions below 1.2:

 $ docker run -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY \ -v /dev/snd:/dev/snd --privileged <image> 

Example â„–1


I thought that some audio player with a graphical interface would be suitable for checking graphics and sound and selected DeaDBeeF as a test subject. To start, we do not need anything except an image with the installed player.

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 collect an image:

 $ docker build -t deadbeef . 

Now you can start it and listen to, for example, 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. The -X when creating an ssh connection enables X11 redirection, which allows displaying a graphical application running on a remote machine on a local machine. In this case, a remote machine can be understood as a docker container.

The ssh server must be installed and running in the container. You should also make sure that X11 redirection is enabled in the server settings. You can check this by looking in /etc/ssh/sshd_config and looking for the X11Forwarding parameter (or its synonyms: ForwardX11 , AllowX11Forwarding ), which should be set to yes :

 X11Forwarding yes 

And the sound?


In ssh, there is no "magic" option to redirect audio. But it is still possible to set it up. For example, using the PulseAudio sound server, for which you can allow client access “from outside” (for example, from a container). The easiest way to do this is through paprefs . After paprefs you need to go into the PulseAudio settings and in the “Network Settings” tab put a tick in front of “Enable network access to local sound devices” (enable network access to local audio devices). You can also 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 ~/.pulse-cookie file into the container.

After changing the settings for PulseAudio, you should restart:

 $ sudo service pulseaudio restart 
or
 $ pulseaudio -k && pulseaudio --start 

In some cases, a reboot may be required. You can check the settings using the pax11publish , the output of which should look something like this:

 Server: <...>unix:/run/user/1000/pulse/native tcp:<hostname>:4713 tcp6:<hostname>: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 the X-applications in the container to connect to our pulse server, you must specify its address in the environment variable PULSE_SERVER:

 $ 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 its IP address 172.17.42.1, where it listens on TCP port 4713.

In general, this setting is sufficient. I will only note that when using the above method, the entire sound will be transmitted in the open (unencrypted) form, which, in the case of using the container on the local computer, does not matter. But if you wish, this traffic can be encrypted. To do this, PULSE_SERVER should be configured to 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 <user>@<hostname> 

The audio data will be transmitted over an encrypted channel.

Example 2


As in the previous example, we describe the image of a DeaDBeF player with an ssh server. I will proceed from the assumption that all the PulseAudio preliminary settings described above have been completed, which means that we can only begin to create a docker image.

The installation code of the player will repeat the code from the example earlier. We just need to add the installation of the openssh server.

This time, in addition to the Dockerfile, we will create another file - entrypoint.sh. This is the script for the “entry point”, i.e. it will be executed each time the container is started. 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"] 

The port and address of the pulse server will be passed as parameters when the container is started, in order to be able to change them to non-standard ones (not 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 collect an image:

 $ docker build -t deadbeef:ssh . 

Run the container, remembering to specify the host PulseAudio (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 

You can find out the user password for connection using the 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 (the connection port will differ from the standard 22nd; in this case it will be 2222 - the one we specified when launching the container). You can check the container's IP using the 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 logging in, you can relax and listen to music:

 dockerx@5e3add235060:~$ deadbeef 


Subuser


Subuser allows you to run programs in isolated docker containers, taking all the work involved in creating and customizing containers, so even people who know nothing 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 the host directories, network, sound devices, etc. In essence, the subuser implements a convenient wrapper over the first method described here for launching graphical applications, since the launch is done by mounting the necessary directories, devices, etc.

Each perimeter is attached with the file permissions.json, which defines the access settings for the application. So, for example, this file looks like for a vim image:

 { "description" : "Simple universal text editor." ,"maintainer" : "Timothy Hobbs <timothyhobbs (at) seznam dot cz>" //       ,"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 (at the moment - a small) ready-made applications, a list of which can be seen using the command:

 $ subuser list available 

Add an application from the repository, you can:

 $ subuser subuser add firefox-flash firefox-flash@default 

This command will install the application, calling it firefox-flash, based on the same image from the default repository.

Running 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. The project code can be found on github: subuser-security / subuser

Example number 3


Let's create a subuser application for the same DeaDBeef. For the demonstration, create a local subuser repository (nothing more than a git repository). The ~/.subuser-repo directory will come down. It should initialize the git repository:

 $ mkdir ~/.subuser-repo $ cd ~/.subuser-repo $ git init 

Here we will 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 in the ability to use the FROM-SUBUSER-IMAGE , which accepts 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 is 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 will describe the access parameters 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 as-root allows you to run applications in a container as root. By default, the subuser starts the container with the --user parameter, giving it the current user ID. But deadbeef at the same time refuses to run (it cannot create a socket file in a home directory that does not exist).

Let's fix the changes in our improvised 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 interaction with it resembles a network one, then a decision immediately comes to mind in the form of remote access systems, such as: TightVNC , Xpra , X2Go , etc.

This option looks more expensive, as it requires the installation of additional software - both in the container and on the local computer. But it also has advantages, for example:

Example 4


As an example, I will use X2Go, because of the solutions I have tested, he liked his ease of use, as well as the built-in support for broadcasting audio. To connect to the x2go-server, a special x2goclient program is x2goclient . More information about server and client installation can be found on the official project website .

As for what we are going to install, you can, of course, install a full-fledged graphical shell like LXDE, XFCE, or even Gnome with KDE in a container. But it seemed to me unnecessary for the conditions of this example. We have enough and OpenBox .

In the container, in addition to the x2go server, you also need an ssh server. Therefore, the code will be in many ways similar to that given in Example 2. In the part where the player, openssh-server, and pulseaudio server are installed. That is, it only remains 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 <item label="DeaDBeeF"><action name="Execute"><execute>deadbeef</execute></action></item>' \ /etc/xdg/openbox/menu.xml ADD entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh EXPOSE 22 ENTRYPOINT ["/entrypoint.sh"] 

We will also slightly modify the entrypoint.sh script. We no longer need to set up the PULSE_SERVER environment variable, so we 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 collect an 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, you should specify the address of either the container itself or the docker host as the host (in this case, you should also consider the non-standard ssh connection port). You can find out the login and password for authorization using the docker logs , as shown in example 2. To start an openbox session, in the “Session type” settings, select “Custom desktop” and enter “openbox-session” in the “Command” field.

After connecting, you can start the player through the openbox menu (right click with the mouse) and check its operation:
screenshot


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


But that's another story.

I also add that X2Go allows you to run single applications, as if they were running on a local machine. To do this, in the client in the settings of the “Session type” select “Single application” and in the “Command” set the path to the executable file. At the same time, there is not even a need to install a graphical environment in the container - all you need is to have an X2Go server and the desired application.

Related Links


Source: https://habr.com/ru/post/240509/


All Articles