Как разрешить с одного хоста управлять Docker-ом на другом хосте? Именно такой вопрос встал передо мной, когда я разворачивал сервисы для управления контейнерами: Portainer и Docker Compose UI. Теоретически, мне было известно, что у Docker имеется API для удаленного управления, оставалось добыть сведения, как разрешить его использование. На самом деле, сделать это совсем не трудно. Довольно быстро нашлось несколько решений, которые, в принципе, сводятся к одному - задать параметры при старте сервиса Docker.

Начну с самого очевидного: надо отредактировать файл, описывающий сервис Docker-а. У меня Docker установлен на виртуалках, на которых, в качестве "гостевых" ОС, используются Debian Jessie и Debian Stretch, в которых, в свою очередь, управление сервисами[1] осуществляется при помощи systemd. А раз так, то для задания параметров запуска сервиса нужно отредактировать файл описания этого самого сервиса.

Согласно документации, файлы описания служб (более правильный темин - unit-файлы) могут находиться в каталогах:

  • /etc/systemd/system - unit-файлы локальной конфигурации
  • /lib/systemd/system - unit-файлы, предоставляемые Debian
    причем те, которые находятся в каталоге /etc/systemd/system имеют более высокий приоритет - они переопределяют такие же файлы из каталога /lib/systemd/system, если, конечно, таковые найдутся.

Так вот, файл, описывающий службу Docker (docker.service), был обнаружен мной в каталоге /etc/systemd/system/multi-user.target.wants (хотя, на самом деле, это была ссылка на /lib/systemd/system/docker.service, но это не так уж и важно, просто... забавно, что ли). Вот эталонное содержимое этого файла, взятое с github-а:

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target docker.socket firewalld.service
Wants=network-online.target
Requires=docker.socket

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd://
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=1048576
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity
# Uncomment TasksMax if your systemd version supports it.
# Only systemd 226 and above support this version.
#TasksMax=infinity
TimeoutStartSec=0
# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes
# kill only the docker process, not all processes in the cgroup
KillMode=process
# restart the docker process if it exits prematurely
Restart=on-failure
StartLimitBurst=3
StartLimitInterval=60s

[Install]
WantedBy=multi-user.target

Нас интересует следующая строка:

ExecStart=/usr/bin/dockerd -H fd://

Именно она задаёт параметры запуска службы Docker и именно её необходимо изменить. Примерно так:

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375

В приведенном выше примере был добавлен параметр -H tcp://0.0.0.0:2375, что означает разрешение взаимодействие со службой Docker по порту 2375 через любой сетевой интерфейс. Если хочется указать конкретный интерфейс, то вместо 0.0.0.0 следует задать IP адрес нужного интерфейса[2].

Итак, файл изменен и сохранен. Для того,чтобы изменения вступили в силу, необходимо "перечитать" unit-файлы. Делается это при помощи команды:

systemctl daemon-reload

Эта команда предназначена для "мягкой" перезагрузки: производится считывание unit-файлов и перестраиваются деревья зависимостей. Но этого недостаточно - для того, чтобы Docker-ом можно было управлять, надо его (сервис) перегрузить:

service docker restart

Следует учитывать, что при перезапуске службы Docker все активные контейнеры будут остановлены и запущены вновь. Поэтому, если есть такая неоходимость, следует озаботиться сохранностью данных в приложениях, связанных с работающими контейнерами. И да, я упоминал, что все вышеперечисленные действия надо выполнять от имени суперпользователя, то есть, root? Если нет, то вот, упоминаю: все эти действия надо выполнять, работая как root, или пользуясь командами sudo или su.

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

Как я уже отмечал, все решения сводятся к одному и тому же - указанию (разрешению) использовать порт 2375 при доступе по протоколу TCP. Только достигается это немного по-разному. Я, например, использовал файл /etc/default/docker. На самом деле, считается, что для *nix систем, работающих с systemd этот каталог (/etc/default/) утратил свое значение и использоваться не должен[3]. Тем не менее, он есть, и в нем даже есть множество файлов, в том числе, и для Docker. Называется этот файл docker (внезапно 😉) и первоначально имеет следующий вид:

# Docker Upstart and SysVinit configuration file

#
# THIS FILE DOES NOT APPLY TO SYSTEMD
#
#   Please see the documentation for "systemd drop-ins":
#   https://docs.docker.com/engine/admin/systemd/
#

# Customize location of Docker binary (especially for development testing).
#DOCKERD="/usr/local/bin/dockerd"

# Use DOCKER_OPTS to modify the daemon startup options.
#DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"

# If you need Docker to use an HTTP proxy, it can also be specified here.
#export http_proxy="http://127.0.0.1:3128/"

# This is also a handy place to tweak where Docker's temporary files go.
#export DOCKER_TMPDIR="/mnt/bigdrive/docker-tmp"

В этом файле все прекрасно - он полностью состоит из комментариев, самыми интересными из которых являются:

...
# Use DOCKER_OPTS to modify the daemon startup options.
...

и

...
# THIS FILE DOES NOT APPLY TO SYSTEMD
...

Из этих двух строк следует, что для изменения параметров запуска dockerd надо описать переменную окружения с именем DOCKER_OPTS, а также то, что сделав это, мы ничего не добьёмся, потому что у нас система с systemd. Но вот ведь какая штука - довольно часто на возникающие вопросы есть ответы. В нашем случае все решается так:

  • в файле /etc/default/docker определяем переменную окружения DOCKER_OPTS[4]:
DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 -H tcp://192.168.0.240:2375 -H unix:///var/run/docker.sock"
  • в файле /etc/systemd/system/multi-user.target.wants/docker.service делаем следующие изменения:
...
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
EnvironmentFile=/etc/default/docker
ExecStart=/usr/bin/dockerd -H fd:// $DOCKER_OPTS
...

Немного поясню суть этих правок. В файле /etc/default/docker все-таки объявляем переменную окружения DOCKER_OPTS, а в unit-файле /etc/systemd/system/multi-user.target.wants/docker.service определяем, что переменные окружения описаны в файле /etc/default/docker и добавляем нужную нам переменную (DOCKER_OPTS) в строку, описывающую параметры запуска сервиса Docker.

Описанный выше метод прекрасно работает, в частности, у меня (после обновления конфигурации и перезапуска сервиса), но это, наверное, не самый правильный подход - решать задачу, используя механизмы, которые официально считаются неприменимыми в существующих реалиях. Естественно, возникает вопрос, а как идеологически правильно должна решаться эта задача.

Один путь решения проблемы основан на использовании механизмов, встроенных в systemd. Идея, лежащая в основе такого подхода, заключается в том, что не следует редактировать файл /etc/systemd/system/multi-user.target.wants/docker.service (который и не файл вовсе, а ссылка на /lib/systemd/system/docker.service), так как его оригинал, так сказать, поставляется разработчиками Docker и, при определенных условиях (обновлении, например), может быть перезаписан, в результате чего внесенные изменения будут потеряны. Вместо этого следует использовать механизм drop-in службы systemd.

Что ж, похоже на разумный подход, тем более, должен признать - однажды у меня такое произошло и после обновления Docker мне пришлось восстанавливать собственные правки в соответствующем unit-файле. Далее я приведу последовательность действий, которые надо выполнить, чтобы разрешить с помощью механизма drop-in, встроенного в systemd, использовать Docker Remote API.

Для начала, необходимо создать в каталоге /etc/systemd/system подкаталог docker.service.d, а в нем - файл с расширением conf, например, docker_override.conf. Таким образом, полный путь к файлу будет выглядеть так:

/etc/systemd/system/docker.service.d/docker_override.conf

Далее, в этом файле надо указать секции и имена свойств, которые надо переопределить (относительно базового unit-фйла). В нашем случае это будет выглядеть, например, так:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375

После сохранения файла выполняем стандартный набор команд:

systemctl daemon-reload
systemctl restart docker.service

Последняя команда (systemctl restart docker.service) аналогична команде service docker restart. По крайней мере, у меня.

Надо сказать, что этот вариант решения я не проверял на Debian Jessie, но смог проверить на Debian Buster, и, должен сказать, что он замечательно работает. Правда, для текущей стабильной версии Debian[5] в unit-файле службы Docker строка ExecStart имеет вид:

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Поэтому, переопределяя конфигурацию для Buster, я всегда дописывал --containerd=/run/containerd/containerd.sock[6]. И вообще, внимательно изучайте оригинальный файл в своей системе, используйте именно его содержимое в качестве основы для модификаций[7].

Приведенный выше метод с использованием механизма drop-in службы systemd не является единственным. В документации Docker приводится описание не только опций командной строки службы dockerd, но и описание специального конфигурационного файла /etc/docker/daemon.json. Исходя из этого описания можно предложить следующий вариант решения проблемы: создать в каталоге /etc/docker файл daemon.json и заполнить его таким содержимым:

{"hosts": ["tcp://0.0.0.0:2375"]}

После перечитывания конфигурации systemd и перезапуска dockerd должно все заработать. Обратите внимание, что я использовал слово "должно" - дело в том, что этот метод я не проверял 😉[8]

Теперь, когда мы предоставили возможность удаленно управлять нашими Docker-машинами, самое время подумать про безопасность этого действа. Хотя, вроде бы, следовало поступить наоборот (то есть, сначала подумать, а уж потом...), но, лучше поздно, чем никогда. Тем более, что само по себе предоставление такого доступа повышает уязвимость системы в общем. Но, если честно, я не очень беспокоюсь. И вот почему.

Помимо всяких примочек, типа NAT на роутере, на входе у меня стоит reverse proxy, который, помимо всего прочего, терминирует SSL трафик. То есть, даже если какой-то сервис и не поддерживает SSL самостоятельно, за него это делает прокси. Таким образом, трафик за пределами локальной сети всегда оказывается зашифрованным.

К тому же, если сервис не содержит встроенных возможностей создания и управления пользователями, эту функцию на себя берет все тот же reverse proxy. То есть, при любом внешнем подключении к сервису (извне локальной сети) будут запрошены имя и пароль пользователя.

Ну и наконец, если уж очень страшно, то можно воспользоваться внутренними возможностями Docker и вместо "открытого" порта 2375 задействовать "защищенный" порт 2376. Правда, придется произвести дополнительные настройки, но ведь спокойствие того стоит? Я же не стал этого делать, так как сервисы, которые должны подключаться к моим Docker-машинам, хостятся у меня же, и трафик этот не покидает пределов локальной сети.

Вот, собственно, пока и всё, что я имею сказать по этому поводу.


  1. Так в системах с systemd принято называть "демонов" (daemons) ↩︎

  2. Параметр -H fd:// означает, что со службой Docker разрешено взаимодействие с использованием механизма Systemd socket activation, и для большинства вариантов локального использования этого вполне достаточно. ↩︎

  3. О том, для чего предназначался этот каталог и почему он стал ненужен очень неплохо написано тут. ↩︎

  4. Я привожу значение в точности в том виде, которое использую сам, но есть предположение, что параметр -H unix:///var/run/docker.sock - лишний ↩︎

  5. Buster является в настоящей момент стабильной версией Debian ↩︎

  6. На самом деле, это связано не с версией Debian, а с версией Docker. ↩︎

  7. О необходимости такого подхода можно почитать тут ↩︎

  8. Небольшое уточнение - проверил я этот подход и столкнулся с проблемами. При перезапуске службы Docker в таком варианте конфигурации выдается ошибка, что параметр задан и в командной строке и в конфигурационном файле, поэтому - все плохо и служба не запускается. Пока дальше не разбирался, может, просто где-то что-то недокрутил. ↩︎