Запуск 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