Docker: launching graphical applications in containers

    Strictly speaking, Docker was not created for this kind of thing, namely the launch of graphical applications. However, from time to time, Docker topics raise questions about whether it is possible to run a GUI application in a container. The reasons may be different, but most often, this is the desire to change an unnecessarily cumbersome virtual machine for something easier, without losing in convenience and while maintaining a sufficient level of isolation.

    This is a short overview of how to run graphical applications in Docker containers.

    Table of contents



    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.0or 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 usingxauth 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 --devicefor connecting devices. Unfortunately, at the moment (version 1.2), --deviceonly 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 --devicewill 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 -Xwhen 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_configand 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 paprefsit, 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-repowill 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-IMAGEthat 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-rootallows 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



    Also popular now: