🏠: docker

Вечер дня сисадмина

После ужина обновил систему на своём сервере-неттопе, а потом решил ещё разик поковырять давно висевшее предупреждение в Nextcloud:

The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If not, this is a security issue and can allow an attacker to spoof their IP address as visible to the Nextcloud. Further information can be found in the documentation.

Сначала я попробовал добавить в параметр trusted_proxies конфигурации Nextcloud не имя контейнера реверс-прокси, а приватный диапазон IP, используемый Докером — 172.16.0.0/12; так много где советуют сделать, но это ничего не дало. В конце концов, проблема решилась заданием того же имени контейнера, но не единым параметром, а первым элементом массива, а также установкой ожидаемого заголовка пересылки.

docker exec -uwww-data nc-php php /var/www/html/cloud/occ config:system:set forwarded_for_headers 0 --value="X-Forwarded-For"
docker exec -uwww-data nc-php php /var/www/html/cloud/occ config:system:set trusted_proxies 0 --value="reverse-proxy"

Наконец-то:

Ободрённый успехом, я решил обновить контейнер с PHP 7.4 на PHP 8.0. Он у меня самосборный — это Alpine c последующей установкой необходимых пакетов и настройкой с помощью командной строки. Я поменял пакеты в Dockerfile на соответствующую версию и поправил пути, пересобрал контейнер, и вроде как заработало, но перестали выполняться команды, вызывающие php, подобные приведённым выше, и остановился cron, выполняющий фоновые задания. Ошибка была такая:

OCI runtime exec failed: exec failed: unable to start container process: exec: ″php″: executable file not found in $PATH: unknown

Полез внутрь контейнера. Команда php8 выполняется, а php — нет. Оказалось, что в дистрибутиве Alpine для php8 не готовы пакеты, поэтому символическая ссылка автоматически при установке не прописывается. Речь об этом шла год назад, не знаю, та же причина сейчас или нет, но результат всё равно один. Добавил ещё одну строчку в Dockerfile, после чего всё заработало:

ln -sf /usr/bin/php8 /usr/bin/php

Cron снова в порядке

Заодно напишу про упомянутый в прошлом тексте выпуск сертификатов Let’s Encrypt для HAProxy. Всё получилось и отлично работает. У Certbot есть каталог /etc/letsencrypt/renewal-hooks/deploy, и если туда положить скрипт, то он будет выполняться после каждого успешного перевыпуска сертификата. В данном случае нужно слепить полную цепочку сертификатов и закрытый ключ в одно целое, положив результат в каталог сертификатов HAProxy:

#!/bin/bash
cat $RENEWED_LINEAGE/{fullchain.pem,privkey.pem} > /etc/ssl/certs/haproxy/$(basename $RENEWED_LINEAGE).pem

# --deploy-hook DEPLOY_HOOK
#    Command to be run in a shell once for each
#    successfully issued certificate. For this command, the
#    shell variable $RENEWED_LINEAGE will point to the
#    config live subdirectory (for example,
#    "/etc/letsencrypt/live/example.com") containing the
#    new certificates and keys; the shell variable
#    $RENEWED_DOMAINS will contain a space-delimited list
#    of renewed certificate domains (for example,
#    "example.com www.example.com") (default: None)

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

#!/bin/bash
if [[ $(find /etc/ssl/certs/haproxy/* -mtime -1) ]]
then
systemctl reload haproxy.service
fi

В cron нужно добавить запуск этого скрипта раз в сутки, например, в 3 часа ночи:

echo -e "\n# Reload HAProxy if there are new certs\n0 3\t* * *\troot\t/scripts/haproxy-reload-if-new-certs.sh" >> /etc/crontab

Ну, с праздничком всех причастных и сочувствующих, спокойной ночи.

Страничка мониторинга опять в строю

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

Конечно, такой вариант мне не годился, но я заметил кое-что интересное, а именно — cAdvisor работает с примонтированным на чтение корнем хостовой системы, чтобы получать информацию собственно о хосте. Тогда я полез в гитхаб-репозиторий своего любимого phpsysinfo и обнаружил, что там появилась инструкция по запуску в Докере, что свидетельствовало о развитии этого направления (я года три не следил за новинками в этой программе), а также плагин, получающий информацию о контейнерах. Не хватало только одного — отображения информации о хостовой системе при работе самого сервиса в контейнере, о чём я написал разработчику, сославшись на подход, применяемый в cAdvisor.

Разработчик оказался невероятно отзывчивым и за 3 дня функционал в виде параметра ROOTFS="/rootfs", позволяющий задавать альтернативный путь к корню, был добавлен, и подправлены ошибки реализации. Настройка phpsysinfo в этом случае немного отличается — везде, где в обычных условиях запрос информации шёл в режиме ACCESS="command", теперь это сделать невозможно, так как из контейнера команды на хост, естественно, не передаются; нужно идти путём ACCESS="data" — когда хост периодически сам выполняет запросы и кладёт файл с результатом в подкаталог <phpsysinfo>/data.

Изображение без описания

Например, для параметров SMART и для Docker нужно добавить в /etc/crontab на хосте примерно следующее:

### phpsysinfo ###
# Docker containers
*/30 * * * * root docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}' > /var/lib/docker/volumes/home_phpsysinfo/_data/mon/data/docker.tmp
# SMART
*/30 * * * * root smartctl --all /dev/sda > /var/lib/docker/volumes/home_phpsysinfo/_data/mon/data/smart0.tmp

Я не стал использовать идущий в комплекте Dockerfile, а сделал по уже привычной схеме: nginx, php-fpm и один именованный общий том.

Изображение без описания

Красота вернулась!

P. S. Совсем забыл: пару дней назад добавил ещё и Watchtower — сервис автообновления образов Докера.

Переезд в Docker

Устройство этого сайта на сегодняшний день

Наконец-то ковыряние Докера привело к чему-то практическому. Конечно, и раньше я ставил его на работе и поднимал там всякие сервисы, но это были одиночные контейнеры и небольшая настройка. Сейчас реализована задача посложнее — переезд с моего одноплатника Orange Pi PC 2, работавшего веб-сервером без малого 4 года (сколько воды утекло с тех пор!), на неттоп, который я купил в 2016 году, завершивший свою карьеру настольного компьютера, со вставленным туда SSD Samsung 850 EVO 250 ГБ, также освободившийся от настольных задач, и смена парадигмы хостинга с монолита на микросервисы.

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

На старом сервере стоял Armbian, веб-сервер Apache и база данных MySQL. Был единый каталог /var/www/html, где в корне лежал Wordpress, а в дополнительно созданных подкаталогах — другие сервисы: в /cloud — Nextcloud, в /wiki — Dokuwiki, в /mon — phpSysInfo, позже был добавлен Webtrees в одноимённую подпапку.

Не то чтобы мне нужно было позарез переезжать на новую платформу, но меня немного беспокоило, что хранилищем выступают флешки, и система стоит на карточке microSD. Хоть логи в Armbian и пишутся в память, тем не менее, ресурс флеш-носителей довольно мал — несколько месяцев назад, например, померла флешка для записи резервных копий. Ну и, конечно, хочется освоить что-то новое и быть в курсе современных прикладных направлений в ИТ. Наконец, желательно иметь более переносимую систему, которая не так привязывается к железу и в перспективе будет работать в кластере.

На неттоп, в котором памяти стало 4 ГБ после обмена с ноутбуком, была установлена Ubuntu Server 20.04 LTS с ядром HWE и Docker в качестве платформы. Затем я перенёс туда наработки, которые я делал на тестовых виртуальных машинах, и занялся миграцией данных.

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

Я всегда стремлюсь к экономии ресурсов, и по возможности использую либо чистый контейнер Alpine Linux (например, для php-fpm — так уж вышло), либо вариант нужного мне сервиса на базе Alpine. Иногда стремление сэкономить выходит боком — я долго возился с «лёгким» веб-сервером Lighttpd, но в случае с Webtrees не смог решить задачу «красивых ссылок» (pretty URLs) даже с помощью специального форума, и в результате решил остановиться на Nginx как самом модном и распространённом варианте на сегодняшний день, для которого везде есть куча конфигураций.

Порой я упирался в непонимание каких-то вещей, например, как раздавать права на томах, если туда смотрят 2 контейнера — nginx и php-fpm, которые работают от разных пользователей? И как раздать эти разрешения с хоста, где таких пользователей вообще нет? Заводить их там не вариант же.

Заставить работать nginx от учётки www-data у меня не вышло, но потом оказалось, что достаточно раздавать права на том с данными только для php-fpm и nginx можно вообще не трогать, а с хоста можно задавать разрешения даже для несуществующих пользователей, если просто указывать совпадающий ID:

sudo chown -R 82:82 /var/lib/docker/volumes/home_cloud/_data/cloud
# Впрочем, правильнее, наверное, так:
docker exec cloud-php chown -R www-data:www-data /var/www/html/cloud

Отдельная песня с реверс-прокси. Например, рабочий конфиг ярлыков для Nextcloud, чтобы внутри него при проверке получить зелёную галочку, оказался такой:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.nc.rule=PathPrefix(`/cloud`,`/.well-known`)"
  - "traefik.http.routers.nc.middlewares=nc-dav,nc-wellknown,nc-sts"
  - "traefik.http.middlewares.nc-dav.redirectregex.regex=(.*)/.well-known/ca(rd|l)dav"
  - "traefik.http.middlewares.nc-dav.redirectregex.replacement=$$1/cloud/remote.php/dav/"
  - "traefik.http.middlewares.nc-wellknown.replacepathregex.regex=^(/.well-known.*)"
  - "traefik.http.middlewares.nc-wellknown.replacepathregex.replacement=/cloud/index.php$$1"
  - "traefik.http.middlewares.nc-sts.headers.stspreload=true"
  - "traefik.http.middlewares.nc-sts.headers.stsseconds=31536000"

И на это уходят дни и недели. Иногда думаешь — да ну всё это к чёрту, потом опять начинаешь долбить эту стену, пока, наконец, не пробьёшся.

Помимо тех сервисов, которые у меня были, я добавил новые:

  • Photoprism — фото- и видеогалерея с распознаванием лиц, геолокацией, распознаванием дубликатов, доступом по ссылкам и т. п.
  • Bepasty — аналог Pastebin, но не только для текста. Можно выкладывать всё, что угодно.
  • Alltube — веб-морда для старого доброго youtube-dl.

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

Есть и убытки — не переехала страничка мониторинга, так как в контейнере она хоста не увидит, а на самом хосте поднимать ради этого веб-сервис глупо. Тандем Prometheus + Grafana — это довольно громоздко, трудоёмко и не очень-то осмысленно ради такого мелкого результата. Посмотрим позже, пока нужно хотя бы наладить какое-то резервное копирование.

Продолжение следует.