Запуск GUI-приложений в Docker-контейнере

Запуск GUI-приложений в Docker-контейнере


Docker обычно используют, чтобы упаковывать фоновые сервисы и консольные утилиты. Но им вполне можно запускать и графические программы! Для этого есть два варианта: использовать уже работающий на хосте X-сервер или поднять VNC-сервер прямо внутри контейнера.

Для начала важно понимать, что именно делает Docker. Контейнер — это способ изоляции, который на первый взгляд похож на виртуальную машину. Но в отличие от виртуалки, контейнеры используют тот же самый ядро Linux, что и хост.

Следующий элемент — X Window System. X-серверы вроде Xorg обеспечивают базовые графические возможности Unix-систем. Без X-сервера GUI-приложения просто не смогут отрисовывать интерфейс. (Есть альтернативные системы, например Wayland, но в этой заметке мы говорим именно про X.)

Запускать X-сервер внутри Docker теоретически можно, но на практике почти никогда не делают. Для этого пришлось бы запускать Docker в привилегированном режиме (--privileged), чтобы дать ему доступ к железу хоста. После старта сервер попробует забрать себе видеоустройства, что обычно приводит к тому, что картинка на хосте пропадает — его родной X-сервер теряет свои устройства.

Куда лучше примонтировать сокет X-сервера хоста в контейнер Docker. Тогда контейнер сможет пользоваться уже существующим X-сервером. GUI-приложения, запущенные внутри контейнера, будут открываться прямо на вашем обычном рабочем столе.

Зачем вообще запускать GUI-приложения в Docker?

Такой способ отлично подходит, когда вы хотите попробовать какое-нибудь новое приложение. Можно поставить его в чистый контейнер, не засоряя хост новыми пакетами.

Кроме того, это помогает избежать конфликтов с уже установленным софтом. Если вам нужно временно погонять две версии одной программы, Docker позволяет сделать это без вечного цикла «удалил — поставил» на хосте.

Пробрасываем X-сокет в Docker-контейнер

Дать контейнеру доступ к X-сокету хоста довольно просто. X-сокет лежит в каталоге /tmp/.X11-unix. Его содержимое нужно примонтировать в контейнер через volume. Для этого также потребуется режим сетей host.

Контейнеру ещё нужно передать переменную окружения DISPLAY. Она говорит X-клиентам — вашим графическим приложениям — к какому X-серверу подключаться. В контейнере DISPLAY должна иметь такое же значение, как $DISPLAY на хосте.

Что такое X Server (на всякий случай, хотя сомневаюсь, что кто-то тут не знает что это)

X server — это оконная система для растровых дисплеев, широко используемая в Linux.

X11-сервер (сейчас чаще всего Xorg) общается с клиентами вроде xterm, firefox и т.п. через надёжный поток байт. Unix-доменный сокет обычно безопаснее, чем TCP-порт, открытый всему миру, и, скорее всего, быстрее — ядро делает всю работу само, без участия сетевых карт или Wi-Fi.

Всю эту конфигурацию можно упаковать в один docker-compose.yml:

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

Далее вам нужно создать Dockerfile для вашего приложения. Вот пример, который запускает браузер Firefox:

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

Теперь собираем и запускаем образ:

docker-compose build
docker-compose up

На рабочем столе должно появиться новое окно Firefox! Этот экземпляр Firefox будет работать внутри контейнера, независимо от любых других запущенных у вас окон Firefox. Контейнер использует X-сокет хоста, поэтому браузер, хоть и контейнеризован, всё равно отображается на вашем рабочем столе.

Но использовать такой подход стоит только тогда, когда вы доверяете контейнеру. Передача хостового X-сервера внутрь — это потенциальный риск безопасности, если вы не уверены, что именно находится внутри контейнера.

Работа с X-аутентификацией

Иногда контейнеру нужно явно разрешить доступ к X-серверу. Для начала получите токен аутентификации на хосте. Выполните xauth list и выберите один из показанных cookie. Скопируйте всю строку целиком.

Внутри контейнера установите пакет xauth, затем выполните xauth add, передав туда скопированный токен.

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

После этого контейнер должен успешно проходить аутентификацию к X-серверу.

Чтобы запустить Firefox, просто выполните firefox в bash внутри контейнера. Вы увидите, что окно браузера появилось на вашем локальном рабочем столе, хотя сам процесс работает внутри Docker-контейнера. По аналогии можно запускать практически любые GUI-приложения в контейнерах и пользоваться ими на своей машине.

Пару слов о рисках X11

В примере выше контейнер получает полный доступ к X-серверу хоста — либо потому, что вы сняли ограничения через xhost, либо потому, что передали контейнеру cookie из xauth. В чём тут проблема? Во-первых, X-сервер должен иметь возможность рисовать на экране, ведь он и создаёт графическое приложение. Это значит, что любое приложение внутри контейнера, которому вы дали доступ к X-серверу, может в любой момент сделать скриншот вашего экрана.

Но и это ещё не всё! X-сервер также имеет доступ к буферу обмена и устройствам ввода — клавиатуре и мыши. Он обязан знать о движениях мыши, нажатиях клавиш и операциях копирования/вставки, чтобы передавать их в GUI-приложения. Поэтому если вы запускаете сторонний продукт внутри контейнера (например, PgModeler), проброс X11-сокета даёт этому коду возможность логировать нажатия клавиш, дергать мышь и читать содержимое буфера обмена.

X11 изначально не проектировали с упором на безопасность, поэтому если вы дали какому-то процессу доступ к X-серверу, у вас нет способа ограничить его возможности. Лучшее, что можно сделать, — минимизировать количество вещей, которые получают такой доступ. То есть не трогать xhost и передавать cookie только тем контейнерам, коду которых вы доверяете. Не пробрасывайте X-сокет в сторонние контейнеры, которые вы не аудитировали!

Другой способ — запуск VNC-сервера

Если пробросить X-сокет не получается, можно поднять VNC-сервер внутри контейнера. Такой подход позволяет смотреть на графические приложения в контейнере через VNC-клиент, запущенный на хосте.

Добавляем ПО для VNC-сервера в контейнер:

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"]

Когда вы запускаете этот контейнер, VNC-сервер поднимается автоматически. Нужно пробросить порт хоста на порт 5900 контейнера — именно на нём сервер будет доступен.

Firefox будет запускаться при старте, потому что он прописан в .xinitrc. Этот файл выполняется, когда VNC-сервер стартует и инициализирует новый дисплей.

Чтобы подключиться к серверу, вам понадобится VNC-клиент на хосте. Узнайте IP-адрес контейнера: выполните docker ps, найдите его ID и передайте его в docker inspect <container>. IP-адрес будет ближе к концу вывода, в секции Network.

Используйте IP контейнера в вашем VNC-клиенте. Подключайтесь к порту 5900 без аутентификации. После этого вы сможете взаимодействовать с графическими приложениями, работающими внутри Docker-контейнера.

Взаимодействие X и Docker

Настольные приложения, запущенные в Docker, будут пытаться общаться с X-сервером, который работает у вас на ПК. Это может происходить как с Docker-движком, работающим на вашем хосте, так и с удалённым движком. Для X особой разницы нет — только задержка по сети может появиться.

Сценарий взаимодействия выглядит так:

Взаимодействие X Windows и Docker

X-клиентам (вашим десктопным приложениям) почти ничего не нужно знать для такого взаимодействия.

На самом деле, им нужно знать только местоположение X-сервера и, по желанию, номер дисплея, на который нужно выводить окно.

Это задаётся переменной окружения DISPLAY со следующим синтаксисом:

DISPLAY=xserver-host:0.

Число после двоеточия — это номер дисплея. В рамках этой статьи можно считать, что «0 — это основной дисплей, подключённый к X-серверу».

Пора запускать наши настольные приложения.

Так как само приложение будет работать внутри Docker-контейнера, а X-сервер — на хосте, нам нужен способ связать их между собой.

К сожалению, универсального готового способа сделать это в Docker пока нет. Поэтому стоит помнить такие настройки для macOS, Windows и 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

Быстрый способ поднять IDE:

Eclipse IDE, запущенная в 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

Report Page