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