Docker от А до Я: мой опыт на домашнем сервере
Docker на домашнем сервере — штука, которую я откладывал годами. Думал, зачем мне ещё один слой абстракции, если есть LXC на Proxmox. Контейнеры же лёгкие, родные, без демонов и всего этого зоопарка. Но когда количество самописных скриптов, вручную настроенных сервисов и костылей для их запуска перевалило за десяток — понял, что так дальше нельзя. Каждый новый сервис означал: создать пользователя, настроить systemd unit, открыть порт, не забыть про автозапуск, прописать логи, разобраться с зависимостями. И при переезде на другой сервер — повторить всё заново. Я устал от этого ещё на пятом сервисе, но упорно продолжал до двенадцатого.
Первым Docker-контейнером у меня стала PostgreSQL. Старая добрая база данных, которая до этого жила прямо на хосте. Сказать, что я напоролся — ничего не сказать. Во-первых, я забыл смонтировать volume для данных. При перезапуске контейнера база просто исчезла. Хорошо, что это был тестовый проект, а не продакшн. Во-вторых, сеть. Контейнер с nginx не видел контейнер с базой, хотя я сто раз проверил IP-адреса. Оказалось, я их запустил в разных сетях. Дефолтный docker-compose.yml я тогда ещё не осилил — всё делал через голые docker run с флагами.
С docker-compose жизнь кардинально изменилась. Один файл, в котором описаны все сервисы, их порты, volumes, сети, зависимости. Ничего не забывается, всё в одном месте, под версиями в git. Сейчас у меня на сервере крутится 12 контейнеров. Это база данных PostgreSQL, n8n для автоматизаций, Uptime Kuma — тулза для мониторинга, о которой я расскажу отдельно, Plausible — легковесная альтернатива Google Analytics, два статических сайта на Nginx, Redis для кеширования, Portainer — веб-морда для управления Docker, MinIO — совместимое с AWS S3 хранилище для бекапов, Watchtower — автоматическое обновление контейнеров (с ним, кстати, надо аккуратно — пару раз прод уронил), Dozzle — просмотр логов в реальном времени, и Home Assistant через контейнер.
Установка Docker на Ubuntu — это три команды. Четыре, если считать добавление репозитория. Но я, как обычно, пошёл сложным путём: скачал deb-пакет с сайта, ручками разрешал зависимости, потом всё снёс и сделал через официальный скрипт. Get.docker.com — там лежит curl-пайп, который сам всё ставит. Многие ругают этот подход за безопасность, но для домашнего сервера — норм. После установки главное — не забыть добавить своего пользователя в группу docker. Иначе каждый раз придётся писать sudo. А это начинает бесить уже на третьем запуске.
Самый частый вопрос, который мне задавали — а зачем Docker, если есть LXC-контейнеры в Proxmox? Отвечаю. LXC хорош, когда тебе нужна изоляция на уровне ОС. Один контейнер — один сервис со своим окружением. Но когда внутри этого контейнера ты ставишь три python-скрипта, каждый со своими зависимостями — начинается ад. Docker решает именно это: каждая программа работает в своём изолированном окружении. Плюс docker-compose — это декларативное описание всей инфраструктуры. Поднял одной командой, убил одной командой. С LXC так не получится.
Что мне нравится в Docker — предсказуемость. Если образ собрался на моей машине, он точно так же заработает на сервере. Никаких «у меня же работает». Никаких «ой, тут пакет другой версии». Я беру образ, пулю контейнер — и получаю тот же результат, что и вчера. Это бесценно, когда ты что-то обновляешь и боишься, что всё сломается.
Watchtower я поставил для автообновлений, но через неделю отключил. Он тупо рестартил все контейнеры по мере выхода новых образов. В один прекрасный день Uptime Kuma обновилась, перезапустилась, а за ней перезапустилась база — и минут пять ничего не работало. Я сидел и смотрел на 502 ошибку в браузере, матерясь сквозь зубы. Теперь Watchtower работает только для некритичных контейнеров, а Proxmox, WordPress и базу данных я обновляю ручками.
Ещё одна грабля — лимиты ресурсов. Если не ограничить контейнер по памяти, он сожрёт сколько дадут. У меня однажды n8n ушёл в своп на 2 гигабайта, потому что я запустил тяжёлый workflow по обработке CSV. Пришлось ставить —memory=512m в docker-compose. И —memory-swap тоже ограничил, чтобы не нагружал диск свопом.
Сейчас мой типовой docker-compose.yml выглядит так: версия 3.8, каждый сервис с рестартом unless-stopped, volume на отдельном диске для данных, сеть bridge для общения между контейнерами, ограничение памяти. Для баз данных — отдельный volume, который я бекаплю вместе с остальными данными через Proxmox. Для конфигов — bind mount, чтобы править файлы напрямую из хоста, если надо.
Что я вынес из этого опыта. Docker не панацея. Он решает конкретную проблему — управление зависимостями и развёртывание. Если у тебя один сервис и он написан на Go — собирай статический бинарник и забудь про контейнеры. Если у тебя зоопарк из десяти сервисов на Python, Node.js и PHP — Docker спасёт тебе месяцы жизни. Главное — не забывай про volumes, ограничивай память и не ставь Watchtower на всё подряд.
Отдельно хочу рассказать про сборку собственных образов. Мой первый Dockerfile был ужасен. Я скопировал туда все зависимости, установил python пакеты через pip, потом понял, что образ весит 1.2 гигабайта. Это потому что я не знал про multi-stage сборку. Сейчас я делаю так: первый этап — сборка приложения со всеми инструментами для компиляции, второй этап — копирование только готового бинарника в чистый образ на alpine. Разница в размере — с 1.2 гига до 150 мегабайт.
Вторая проблема — кеширование слоёв. Если ты меняешь исходный код, но не меняешь зависимости — Docker пересобирает только слои, которые изменились. Но если ты написал COPY . . до установки зависимостей — кеш сбрасывается каждый раз, даже если ты просто пробел в README поправил. Я долго тупил на этом, пока не понял порядок инструкций в Dockerfile. Сначала COPY requirements.txt, потом RUN pip install, потом уже COPY всего остального. Тогда зависимости кешируются, а пересобирается только код.
Ещё момент — .dockerignore. Без него в образ улетает всё: node_modules, .git, временные файлы. Я как-то залил образ с node_modules на 400 мегабайт. В продакшне, конечно, это не нужно. Теперь в каждом проекте лежит .dockerignore с минимальным набором исключений.
Логирование. В контейнерах по умолчанию драйвер json-file, который пишет логи в файл. Если не ограничить размер — он может занять весь диск. У меня так было: докер жрал по 10 гигабайт на логи одного контейнера за месяц. Пришлось добавить в daemon.json настройки max-size=10m и max-file=3. И для чувствительных контейнеров поставил драйвер local — он с ротацией из коробки и меньше грузит диск.
Проблемы с временем. Если внутри контейнера часы расходятся с хостом — начинается веселье. Логи не совпадают по времени, сертификаты якобы протухли, cron срабатывает не тогда. Решение — пробросить /etc/localtime как volume или добавить часовой пояс в переменные окружения через TZ=Europe/Moscow. Я предпочитаю второй вариант.
Про бекапы. Контейнеры эфемерны — это их фишка. Если контейнер упал, Docker просто перезапускает его. Но данные в volumes должны бекапиться отдельно. У меня настроен ежедневный бекап всех Docker volumes вместе с Proxmox дампом. И раз в неделю — синхронизация на отдельный диск через rsync. Потому что если диск помрёт — все контейнеры пересоздадутся, а данные — нет.
Что касается Docker Compose — это лучшая штука для домашнего сервера. Один файл на весь проект, всё описано декларативно. Я храню все compose-файлы в отдельной папке /opt/docker, каждый сервис в своей подпапке. Поднял — работает. Уронил — docker compose down и пересобрал. Без танцев с бубном и воспоминаний «а как я это настраивал в прошлый раз».
Теперь я делаю так. Если новый сервис можно запустить в Docker — я запускаю его в Docker. Исключение — сам Proxmox и критические системные службы. Всё остальное: БД, веб-серверы, утилиты, парсеры — живут в контейнерах. И я перестал бояться слов «оно не запускается».