Домашний сервер задумывался как тестовый стенд рабочих сайтов, поэтому схему взяли с боевого сервера один в один: обратный прокси traefik принимает все запросы, у каждого сайта свой набор контейнеров nginx + PHP + MariaDB, крон запускает ofelia по меткам контейнеров. «Просто поднять так же» не вышло: часть схемы на свежем Docker не заработала вовсе, а часть заработала, но вела себя иначе, чем мы думали годами.

Это пятая часть цикла «Домашняя инфраструктура».

traefik 2 не видит Docker 29

Первый же запуск: прокси поднялся, а ни одного сайта не видит. В журнале — провайдер Docker не может подключиться. Причина в версиях API: traefik 2 (и 2.10, и последняя 2.11) обращается к Docker по API 1.24, а Docker 29 принимает только 1.40 и новее. Переменная DOCKER_API_VERSION не помогает: версия зашита в клиент.

Поставили traefik 3. Статический конфиг от второй версии он принял без правок. А вот правила маршрутов пришлось переписать.

Если на вашем сервере traefik 2 — обновите его до того, как обновится Docker. Иначе после обновления пакетов прокси перестанет видеть контейнеры, и лягут все сайты разом.

HostRegexp в traefik 3 — обычное регулярное выражение

Во второй версии поддомены ловились шаблоном с именованной группой {subdomain:[a-z]+}. В третьей этот синтаксис удалён, теперь это обычное регулярное выражение Go:

labels:
  - 'traefik.http.routers.site.rule=HostRegexp(`^([a-z0-9-]+\.)?example\.ru$`)'
  - 'traefik.http.routers.site.tls.certresolver=letsEncrypt'
  - 'traefik.http.routers.site.tls.domains[0].main=example.ru'
  • в YAML такое правило — только в одинарных кавычках: в двойных \. даёт ошибку «unknown escape»;
  • по регулярному выражению traefik не может сам понять, на какое имя выпускать сертификат, поэтому нужен явный tls.domains[0].main.

Имя базы данных — это домен сайта

В наших конфигах сайтов DB_HOST равен доменному имени сайта, а не localhost и не имени контейнера. Работает это потому, что контейнер MariaDB получает в сети Docker псевдоним, совпадающий с доменом. Из этого вытекают два следствия, которые стоит знать заранее:

  • вешать тот же домен псевдонимом ещё и на прокси нельзя: встроенный DNS Docker начнёт отдавать два контейнера вперемешку;
  • запрос PHP к собственному сайту по домену попадёт не на веб-сервер, а в базу. Поэтому крон запускается командой php -f wp-cron.php внутри контейнера, а не HTTP-запросом к сайту.

Без имени проекта compose сносит соседей

Файлы compose у нас лежат в каталоге docker/ каждого проекта. Если не задать COMPOSE_PROJECT_NAME, имя проекта берётся из имени каталога — то есть у всех будет «docker». Прокси и служебные контейнеры становятся одним проектом, и docker compose down в одном каталоге гасит контейнеры другого. Имя проекта — обязательная строка в .env каждого.

stop не выключает сайт

Чтобы тестовая копия не ела ресурсы, я гасил её через docker compose stop. После очередного systemctl restart docker все девять остановленных контейнеров поднялись сами: у них restart: always, а эта политика срабатывает при старте демона, даже если контейнер останавливали вручную.

# выключить надолго: контейнеры удаляются, данные остаются
docker compose -p site-test down          # без -v: именованные тома и bind-mount базы целы
# включить обратно
docker compose -p site-test up -d

unless-stopped переживает ручную остановку, always — нет. Если хотите, чтобы stop держал контейнер выключенным, нужна именно unless-stopped.

ofelia не замечает новые сайты

Ofelia читает метки контейнеров только при своём старте. Подняли новый сайт или пересоздали контейнер — задания крона для него не появятся, пока не перезапустить саму ofelia. Перечитать метки на лету она не умеет. Хуже того: после перезапуска Docker ofelia стартует раньше сайтов, не видит ни одного задания, и крон молча не работает нигде.

Написал маленький сторож: он слушает события Docker и перезапускает ofelia, когда запускается или падает контейнер с её метками.

#!/bin/bash
# ofelia-watch: перезапуск ofelia при старте/падении контейнеров с метками крона
sleep 25 && docker restart ofelia >/dev/null          # стартовая синхронизация после подъёма docker
last=0
docker events --filter label=ofelia.enabled=true --filter event=start --filter event=die \
  --format '{{.Actor.Attributes.name}}' |
while read -r name; do
  [ "$name" = "ofelia" ] && continue                  # у самой ofelia та же метка — иначе бесконечный цикл
  now=$(date +%s); [ $((now - last)) -lt 8 ] && continue   # пачка событий — один перезапуск
  last=$now; docker restart ofelia >/dev/null
done

Docker 29 считает образы по-другому

В Docker 29 по умолчанию хранилище образов containerd. IMAGE ID теперь — дайджест манифеста, а не конфигурации, и размеры учитываются иначе: один и тот же образ на старом сервере показывал 2,44 ГБ, на новом — 1,74. Сравнивать, тот ли образ стоит на двух машинах, можно только по RepoDigests или версиям внутри, но не по ID и не по размеру. И образы лежат в /var/lib/containerd, а не в /var/lib/docker — учитывайте это, когда ищете, куда делось место.

Сборка фронта без Node на сервере

Node ставить на хост не стал: версия из репозитория Ubuntu — 22, а наша сборка требует 20 (в 22 удалён синтаксис импорта JSON, на котором держится один из плагинов). Вместо этого обёртка node-docker запускает команды в контейнере node:20 от моего пользователя, с HOME=/tmp, лимитом памяти и --init.

Сборщик пишет результат выше текущего каталога (../assets-dist). Если смонтировать в контейнер только текущую папку, файлы уйдут в никуда внутри контейнера, а в журнале будет «compiled successfully». Монтировать надо корень проекта, по тому же абсолютному пути, что на хосте.

Переносите схему с одного сервера на другой?

Пришлите compose и конфиг прокси — проверю, что сломается на новых версиях, до того как это случится.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.