понедельник, 30 мая 2016 г.

Заворачиваем Chrome в Docker контейнер

Сегодня будем заниматься засовыванием Chrome в Docker контейнер. Я этим занялся, поскольку планирую перенести пакеты, которые не входят основной репозиторий - в контейнеры. Это безопаснее, реже ломается, быстрее в развёртывании и обновлении, не тянет ненужных зависимостей. Плюс интересно приобрести опыт в работе с контейнерами, которые имеют GUI. Chrome хороший, в этом плане, кандидат - много неочевидных нюансов в настройке. Приступим.

Цель

В отличии от множества примеров в интернете, я хочу иметь локальное соединение с X11, без использования ssh. Я ленивый и поэтому шрифты, курсоры и прочее должно браться из настроек хостовой машины, а не настраиваться внутри. Запускаться всё это безобразие должно максимально просто, без сложных настроек моей машины.

 

Настройка

Сначала тезисно расскажу что нужно сделать, а ниже объясню что эти заклинания значат. Добавьте текущего пользователя в группу "docker", что бы работать с docker без sudo, это удобнее и инструкции ниже даны в расчёте на это предположение.

Создаем файл "Dockerfile" вот с таким содержимым
FROM debian:jessie

ADD https://dl.google.com/linux/linux_signing_key.pub /tmp/
COPY run_chrome /bin/
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && \
  apt-get install -y --no-install-recommends dbus-x11 libexif12 && \
    chmod +x /bin/run_chrome && \
  apt-key add /tmp/linux_signing_key.pub && \
  echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' >> /etc/apt/sources.list && \
  apt-get update && \
  apt-get install -y google-chrome-stable && \
  apt-get clean -y && \
    groupadd --gid 9999 docker && \
    useradd --password='' --uid=9999 --gid=docker --shell=/bin/bash --create-home docker

CMD /bin/run_chrome
Рядом должен лежать файл "run_chrome" со скриптом запуска:
#!/bin/bash

EXISTS=$(getent group hostgroup)
if [ "$EXISTS" == "" ]; then
groupadd -g $HOST_GID -o hostgroup
usermod -u $HOST_UID -g $HOST_GID docker
chown docker:hostgroup \
    /home/docker/.config/ \
    /home/docker/.config/google-chrome \
    /home/docker/.cache \
    /home/docker/.cache/google-chrome \
    /home/docker/Downloads
fi

su - docker -c google-chrome
Собираем вот так (в той же директории, где лежат предыдущие 2 файла):
docker build -t chrome .
Запуск контейнера сложен, поэтому вынесем в отдельный "chrome.sh":
#!/bin/bash

STATE=$(docker ps --all --filter="name=chrome" --format="exists")
if [ "$STATE" == "" ]
then
docker run -d \
  --name=chrome \
  --cap-add=SYS_ADMIN \ 
  --security-opt seccomp:unconfined \
  --net=host \
  --env="DISPLAY" \
  --env="HOST_UID=$(id -u)" \
  --env="HOST_GID=$(id -g)" \
  --device /dev/snd \
  --log-driver=journald \
  --volume="/etc/localtime:/etc/localtime:ro" \
  --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
  --volume="/dev/shm:/dev/shm:rw" \
  --volume="$HOME/downloads:/home/docker/Downloads:rw" \
  --volume="/usr/share/icons:/usr/share/icons:ro" \
  --volume="/usr/share/fonts:/usr/share/fonts:ro" \
  --volume="/etc/fonts:/etc/fonts:ro" \
  --volume="/var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket:ro" \
  --volume="$HOME/.Xauthority:/home/docker/.Xauthority:ro" \
  --volume="$HOME/.cache/google-chrome-cache:/home/docker/.cache/google-chrome:rw" \
  --volume="$HOME/.cache/google-chrome-config:/home/docker/.config/google-chrome:rw" \
  chrome
else
  docker start chrome
fi
Готово, даём права на исполнение и запускаем:
chmod +x chrome.sh
./chrome.sh

 

Объяснения

Для практических целей пункта выше достаточно, кому интересно, я объясню что значат скрипты выше.

Установка пакетов в "Dockerfile" вопросов вызывать не должна, "dbus-x11" и "libexif12" явно устанавливаются, потому что chrome без них выдаёт пару лишних warning, но работает и без них.

Работать из под root не рекомендуется, а некоторые программы отказываются это делать, поэтому внутри создаётся пользователь "docker" с уникальными UID и GID. При старте, в "run_chrome" они меняются на те, что используются на хостовой машине, для этого в параметрах запуска передаются HOST_UID и HOST_GID. Теперь кеш и конфиги браузера, которые замаплены на вашу файловую систему будут иметь те же права, что и у вашего пользователя. Мы ведь хотели бесшовной работы.

Что бы подключиться к X11 серверу нужно пробросить в контейнер сокет "tmp.X11-unix" и переменную окружения "DISPLAY". Удалённые подключения к нему по умолчанию не разрешены, что бы обойти проблему, дополнительно даём доступ на чтение "$HOME/.Xauthority", а сеть не виртуализируем (параметр "–net=host" при запуске). В качестве альтернативы можно было разрешить все локальные подключения, выполнив на хосте:
xhost +local:
Устройство "/dev/snd" нужно, что бы заработал звук.

Chrome по умолчанию создаёт "песочницы" для некоторых своих дочерних процессов, что бы изолировать их и повысить безопасность. Этакий docker внутри docker получается. Но права на это по умолчанию не выдаются. Можно было бы запустить контейнер с флагом "–privileged", что часто решает проблемы подобного рода. Но более дискретным будет выдать нужные разрешения флагами "–cap-add=SYS_ADMIN" и "--security-opt seccomp:unconfined". Как вариант, можно не давать разрешения и запустить сам chrome с флагом "–no-sandbox", отключающим песочницу, что делать не советуют.

Для того, что бы запускать хром с любимыми курсорами, шрифтами и выставить timezone, добавляем volumes: "/usr/share/icons", "/usr/share/fonts", "/etc/fonts" и "/etc/localtime".

Директорию "/dev/shm" я замапил, поскольку chrome иногда падает, например при открытии google maps, из-за недостаточного размера "/dev/shm" выставленного по умолчанию. Решение нашёл вот тут, там же подробнее описаны симптомы.

Сокет "/var/run/dbus/system_bus_socket" пробрасывать не обязательно, но раз уж хочет chrome работать с dbus, почему бы не дать ему это сделать?

Опция "–log-driver=journald" нужна, что бы логи записывались в "journalctl". По умолчанию они пишутся в отдельный файл и кстати не ротируются, в результате могут занимают много места, если вы редко пересоздаёте контейнер. Посмотреть логи Chrome в journald можно вот так:
sudo journalctl CONTAINER_NAME=chrome
Интересно посмотреть на логику скрипта "chrome.sh", он создаёт контейнер только при первом запуске. Это относительно долгая операция. При следующем, когда контейнер уже существует, он запускается командой "docker start chrome", что работает сильно быстрее.

 

Производительность

Десяток секунд занимает только первый запуск, как я сказал выше. Последующие - отрабатывают почти мгновенно. Производительность работы самого chrome - не отличается от нативно установленного. Я проверял при помощи вот этого теста, разница была 0.3% - на уровне погрешности.

 

Итоги

В результате у нас получилось самодостаточное приложение, внешне ничем не отличается от явно установленного, но при этом оно работает изолировано от основной системы. Плюс оно не несёт дополнительных зависимостей (ну кроме docker). Получилось похоже на обсуждаемые в последнее время snap-пакеты, появившиеся в Ubuntu. Только куда гибче в настройке.

Если таким образом планируется оборачивать больше одного приложения - вынесите общие зависимости в базовый образ. Это сэкономит место на диске.

Комментариев нет:

Отправить комментарий