Почему всё началось

Я являюсь фанатом self-hosted сервисов. Это интересное и, на мой взгляд, полезное хобби. Одним из самых простых способов поднять на своём оборудовании какой-нибудь сервис - воспользоваться соответствующим Docker-образом, желательно, официальным, ну или, хотя бы, популярным. Но что дальше? То есть, вот вы установили сервис и используете его, но ведь авторы не собираются останавливаться. Они исправляют ошибки, добавляют новые... возможности 😉. И рано или поздно приходит момент, когда сервис надо обновить. Docker и Docker Compose позволяют сделать это достаточно просто и эффективно. Надо лишь каким-то образом понять, что у используемого сервиса появилась новая версия.

Как всё было в самом начале

Очень долгое время я обновлял свои сервисы полностью в ручном режиме. Причём алгоритм моих действий менялся по мере того, как я набирался опыта, иногда, достаточно болезненного. Надо отметить, что достаточно быстро я пришёл к решению использовать Docker Compose даже для простых сервисов, основанных на единственном образе и не связанных с другими сервисами. Объяснение этому очень простое - мне нравится возможность конфигурировать сервис при помощи YAML файла, а не писать длиииинюююющие команды с указанием большого количества параметров.

Благодаря этому решению, для обновления сервисов я использовал и продолжаю использовать очень простую последовательность команд:

docker-compose stop
docker-compose pull
docker-compose up -d

Эта последовательность предполагает, что текущим каталогом является каталог, в котором размещён файл конфигурации обновляемого сервиса - файл с именем docker-compose.yml.

В результате, если используемый образ сервиса, или одного из его составных частей, обновился, то при выполнении команды pull он будет скачан, распакован и полностью готов к использованию. Останется лишь вновь его запустить. Если же обновлений нет, то ничего не произойдёт и при запуске будет использоваться старая версия.

Как стало чуть позже

Но потом я отказался от этого подхода. Нет, конечно, я по-прежнему использую эти три команды. Просто теперь я не вызываю их без предварительных манипуляций. Что это за манипуляции? Всё достаточно просто. Я захожу на Docker Hub, или другое хранилище образов, нахожу используемой мной сервис (или набор сервисов), смотрю, есть ли новые версии. Если нет - что ж, попробуем в следующий раз. Если же есть, я ищу информацию о том, что именно изменилось. Достаточно часто для этого приходится посетить домашнюю страницу самого ПО, упакованного в Docker образ.

Для чего мне это нужно? Всё просто. Я стал так поступать после того, как пару раз, в результате "слепого" обновления, сталкивался с тем, что обновлённый сервис переставал работать. Если вы просто балуетесь - это не проблема. Но если сервис критичен для вас, и в нём успело накопиться достаточно много важных данных, потеря работоспособности может серьёзно вас огорчить. Как меня когда-то.

И ладно, если можно исправить ситуацию простым откатом на предыдущую версию. Так зачастую можно поступить, если используемый сервис не хранит данные. Например, Only Office - это всего лишь набор офисных программ для редактирования документов. Но если сервис хранит данные, и в результате обновления эти данные подвергаются определённым изменениям, чтобы соответствовать новым условиям использования, то просто так откатиться к старой версии может не получиться. Нет, конечно, предыдущую версию ПО вернуть можно, вот только с уже изменёнными для новых условий данными эта устаревшая версия может и не захотеть работать.

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

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

Как всё началось

Началось всё, как обычно - я писал текст для своего блога, искал какую-то информацию, захотел сохранить закладку на какой-то из просматриваемых материалов в своём сервисе закладок (shaarli), вспомнил, что достаточно давно не могу с телефона подключиться к сервису при помощи программы shaarlier, решил посмотреть, нет ли новой версии сервиса. Обновление нашлось, но, к сожалению, ничего не поменялось - авторизация всё так и не проходит. Потом вспомнил, что достаточно давно не обновлял ghost, а ведь он достаточно часто получает обновления, поэтому почитал, что нового в нём напрограммировали. Обновил. Потом пришла очередь NextCloud. Дальше - больше: Wallabag, Wekan, Dokuwiki... В общем, как и говорил, обычное дело.

Немного конспирологии

И тут я в очередной раз подумал, что надо бы как-то автоматизировать процесс определения того, какие из моих сервисов получили обновления. И, буквально, минут через 10-15 мне попадается занятная статья по интересующей меня теме. Какое странное совпадение: подумал - подвернулась статья... Раньше ведь как было, напишешь кому-нибудь письмо, или, наоборот, получишь сообщение, упомянешь какую-то тему - всё, получай в поиске рекламу. Потом, стал замечать, что иногда для получения рекламы оказывается достаточным поговорить по телефону или просто в машине обсудить с женой ближайшие планы. А теперь вот только подумал. Может, Гугл теперь и так умеет😯? На всякий случай, я подумал ещё про пару-тройку вещей. К счастью - без последствий: надо со своей паранойей что-то делать 😂. Тем не менее, продукт, описанный в статье, я решил опробовать.

Watchtower

И надо сказать, я оказался впечатлён возможностями этого ПО. Если кратко, то с его помощью можно мониторить обновление образов, на которых основаны работающие у вас контейнеры. И не только мониторить, но и автоматически обновлять. То есть, используя Watchtower, можно, теоретически, всегда иметь самые свежие версии используемого ПО.

Если честно, у меня были опасения по режиму автоматического обновления - выше я уже писал, что достаточно давно и вполне осознанно перестал бездумно обновлять образы используемых сервисов. К счастью, Watchtower позволяет ограничиться только мониторингом. Но, обо всём - по порядку.

Запуск

Для запуска Watchtower надо скачать подготовленный образ и запустить контейнер. У продукта достаточно широкий список возможностей, и какие именно и как вы будете использовать, определяется набором управляющих аргументов. К счастью, все аргументы можно задать и как переменные окружения.

Так как я фанат задействовать Docker Compose для запуска сервисов, то решил и в этом случае не изменять своим правилам. Итак, начнём.

На Docker Hub есть несколько образов Watchtower. Я остановил свой выбор на образе containrrr/watchtower. Причина достаточно проста - он был самым свежим и постоянно обновлялся. К тому же, это, видимо, основной разработчик/контрибутор.

Монтирование

Так, с образом определились, пришло время настроек. Первое, что необходимо сделать - предоставить возможность приложению взаимодействовать с Docker. Для этого необходимо примонтировать к контейнеру /var/run/docker.sock. Вот она, затравка нашего файла конфигурации:

version: '2'

services:

  watchtower:
    image: containrrr/watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

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

Настройка периодичности

Watchtower позволяет указать, когда необходимо обновлять образы. Для этого можно использовать две взаимоисключающие стратегии: обновлять просто через определённые интервалы времени, и обновлять по расписанию. По умолчанию, если ничего не указывать, будет использоваться интервал времени. Его можно указывать в секундах и, опять же, по умолчанию, он равен трёмстам секундам. Этот режим, как мне кажется, больше подходит для разработчиков - они постоянно что-то изменяют и "пушат" эти изменения в реестр или в локальный кеш. Естественно, при этом хочется, что бы новый образ был бы быстрее обновлён, например, для целей тестирования. Для простых же "хостеров" сервисов, таких, как я, больше подходит режим обновления по расписанию. Для указания расписания используется строка, записанная по правилам cron expression, что позволяет указывать достаточно гибкие расписания. Мне большая гибкость была не нужна, по крайней мере пока, поэтому я остановился на обновлении каждые 24 часа. Для указания расписания я решил воспользоваться переменными окружения. Вот, что начало получаться:

...
    environment:
...
      - TZ=RU
      - WATCHTOWER_SCHEDULE=0 0 0 * * *
...

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

Только мониторинг

Я уже отмечал, что с осторожностью отношусь к "слепым" обновлениям, предпочитая почитать и понять, что из себя представляют изменения в ПО, и есть ли вероятность, что что-то может сломаться. К счастью, Watchtower позволяет просто мониторить появление обновлений, не производя самого обновления. Для перевода его в такой режим работы, надо использовать аргумент monitor-only. Тут хочу показать ещё один способ указания аргументов при конфигурировании - при помощи параметров командной строки:

...
    command: --monitor-only
...

То есть, в элементе command можно указывать аргументы точно в том виде, как они указываются в командной строке.

Про сам режим мониторинга надо знать ещё одну вещь - образы, как утверждается, из-за особенностей API Docker, всё равно скачиваются. Это надо учитывать по двум причинам. Во-первых, Docker Hub ввёл ограничения для анонимных пользователей, а также для пользователей на бесплатном тарифе. Поэтому мониторинг мониторингом, но счётчик скачиваний будет увеличиваться. А во-вторых, из-за того, что образ уже скачан, само обновление происходит удивительно быстро, и, возможно, неожиданно - после остановки/запуска контейнера (а я для запуска использую docker-compose up -d), он будет пересоздан с использованием нового образа. Чтобы этого не происходило, документация, в случае с docker-compose, предлагает использовать аргумент --no-recreate.

Оповещение

Так, теперь Watchtower будет мониторить появление новых версий тех образов, из которых созданы контейнеры, работающие на хосте. Ну и что? Промониторить мало, надо ещё как-то оповестить. И это Watchtower тоже умеет. Надо указать, каким образом вы хотите получать оповещения. Для этого надо воспользоваться аргументом notifications, который может принимать следующие значения:

  • email - для отправки сообщений по электронной почте
  • slack - сообщения будут отправляться через webhook Slack-а
  • msteams - сообщения будут отправляться при помощи MSTeams webhook
  • gotify - для отправки сообщений через Gotify
  • shoutrrr - для отправки сообщений через containrrr/shoutrrr

Я, ожидаемо, решил получать оповещения старым дедовским способом - через электронную почту. Это выбор повлёк за собой целый каскад настроек, необходимых, чтобы Watchtower смог отправить сообщение. Вот, как это выглядит в файле настроек:

...
    command: --monitor-only --no-startup-message --notifications email
...
    environment:
      - WATCHTOWER_NOTIFICATIONS_LEVEL=info
      - WATCHTOWER_NOTIFICATION_EMAIL_TO=кому@отправлять.письмо
      - WATCHTOWER_NOTIFICATION_EMAIL_FROM=откого@отправлять.письмо
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=пользователь@gmail.com
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=пароль-пользователя-gmail
      - WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG=Префикс_темы:
      - WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2
...

Совсем немного пояснений. Для начала, указываем, какие сообщения хотим получать: Watchtower, по сути, в качестве оповещений отправляет сообщения своего журнала работы, и вам даётся возможность указать, какую детализацию журнала вы хотите получать. Доступные следующие уровни детализации: panic, fatal, error, warn, info, debug или trace. Я выбрал info, что подразумевает получение не только информационных сообщений, но и сообщений об ошибках. А вот более детальных сообщений я получать не хотел и не буду.

Ясно, что надо указать получателя сообщений - ваш почтовый ящик. Параметр, определяющий, от имени кого будут отправляться сообщения, и параметр, определяющий пользователя почтового сервиса (для аутентификации), обычно совпадают, по крайней мере, у меня. С паролем всё понятно, наверное. Далее используемый почтовый сервис - я использую gmail, вы указываете свой, ну и порт (587). Префикс темы письма позволяет хоть как-то кастомизировать стандартный генерируемый текст для темы сообщения. Последний параметр - задержка отправки письма (в секундах) - я не очень понял, что это такое, и для чего нужно, но в примере этот параметр был указан, указал его и я 😉. Ну и самые внимательные, наверное, заметили в элементе command аргумент --no-startup-message - он нужен для того, чтобы не получать сообщение о том, что Watchtower начал работать - такое сообщение имеет уровень info, а этот флаг позволяет не забивать почту ненужными сообщениями.

Конфигурация

В конце концов, у меня получился вот такой файл конфигурации:

version: '2'

services:

  watchtower:
    command: --monitor-only --no-startup-message --notifications email
    image: containrrr/watchtower
    environment:
      - WATCHTOWER_NOTIFICATIONS_LEVEL=info
      - WATCHTOWER_NOTIFICATION_EMAIL_TO=xxxx@xxxxxx.com
      - WATCHTOWER_NOTIFICATION_EMAIL_FROM=noreply@xxxxxx.com
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=noreply@xxxxxx.com
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=xxxxxxxxxxxxxxx
      - WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG=Servers WatchTower> 
      - WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2
      - TZ=RU
      - WATCHTOWER_SCHEDULE=0 0 0 * * *
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Что ещё возможно

Конечно, это далеко не все возможности Watchtower. Из наиболее востребованных можно упомянуть, например, ещё такую - работа с контейнерами на удалённом хосте. Для указания нужного хоста используется аргумент --host или же переменная окружения DOCKER_HOST. В качестве значения указывает адрес хоста, примерно, в таком виде: tcp://10.0.1.2:2375. Но у такой функции есть обязательное предусловие - на удалённом хосте должно быть разрешено внешнее управление Docker-ом.

Ещё одна интересная функциональность - возможность создания групп обновления и запуск нескольких экземпляров Watchtower, каждый из которых отвечает за обновление своей группы. Достигается это следующим образом: для контейнеров указывается специальная метка com.centurylinklabs.watchtower.scope, значение этой метки у контейнеров одной группы одинаково; у экземпляра же Watchtower, который должен обновлять данную группу, это же значение указывается для аргумента --scope. Для чего это может понадобиться? Ну, например, для обхода новых ограничений Docker Hub - если у вас много контейнеров, то можно использовать тот факт, что в сутках 24 часа, то есть, 4 раза по 6 часов. Таким образом можно, теоретически, разбив контейнеры на группы, развести их по времени обновления, создав видимость того, что можно обновить больше образов 😏.

Что имеем сейчас

В общем, софт довольно интересный и я только начал его использовать. Наверное, впереди меня ожидают новые открытия, ну и разочарования, куда ж без них. Но пока это всё, что могу пока рассказать из собственного опыта.

Ах, да, чуть не забыл... Вот пример письма, приходящего мне на почту:

Services WatchTower> Watchtower updates on XXXXXXXXXXXX
Inbox

noreply@xxxxx.com
3:01 PM (7 hours ago)
to me

2020-12-12 12:00:55 (info): Found new nextcloud:latest image (sha256:3c7dd0564a34fa33f4061f2e89ef93865fb378e97ff3f0b2bbcc594888238cd7)
2020-12-12 12:01:02 (info): Found new redis:latest image (sha256:ef47f3b6dc11e8f17fb39a6e46ecaf4efd47b3d374e92aeb9f2606896b751251)
2020-12-12 12:01:11 (info): Found new wallabag/wallabag:latest image (sha256:718768ad1785e82510f3d84eac4e0e53771cfd51e4fd37d613a1d07f754a8a18)
2020-12-12 12:01:21 (info): Found new redis:alpine image (sha256:f731cd48185c2bc96057d5bc76e2cdb398dee800c7608bc3ec52e423d27e0f0a)
2020-12-12 12:01:24 (info): Found new mongo:4.2 image (sha256:511fe7789ae9bcedbe831171699aa524af2d4d3de37b5b7afdca74510c2a3d0a)
2020-12-12 12:01:34 (info): Found new linuxserver/transmission:latest image (sha256:603857e3043de97d6ebf28fce03eb92eb4763e73ab1e9ca99e518cffa73694b6)

Вот теперь, пожалуй, всё... пока...