В январе 2023 мне пришла в голову идея: а почему бы не управлять своими серверами так же, как я управляю своими проектами — через docker compose up.

Довольно быстро стало понятно, что до меня в эту сторону массово не ходили, если кто так уже делает, то делает это молча… т.е. на все детские грабли на этом пути мне придётся наступить лично.

А вот вам повезло: если тоже захотите пойти в эту сторону, то у вас уже есть и эта статья и пример конкретной реализации.

Кому этот подход может подойти? Тут должны сойтись звёзды несколько факторов:

  • Нужно быть программистом, хорошо знакомым с docker-compose.yml.

  • Нужно иметь 1-5 личных серверов — не важно, дома или на обычном/облачном хостинге, настоящий ли это сервер или свой десктоп/ноут, выполняющий заодно и функции «сервера».

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

  • Но главное — нужно не быть админом, которому Ansible привычнее. ?

Что касается IaC, то код проектов мы тоже когда-то давно писали без git, нередко меняя его прямо на сервере, и хорошо если через scp, а не в текстовом редакторе прямо на сервере.
А потом стало очевидно, что это плохая идея, и git действительно нужен даже для личных проектов.
Не буду долго агитировать за IaC, просто спрошу: вот у вас дома «сервер», и там винт умер — вы же сможете легко и быстро настроить новый сервер с нуля так же, как был настроен старый? Если ответ "да", то дальше можете не читать, у вас уже всё хорошо!

Лично меня в этом подходе привлекла его крайняя простота: всё, что для него нужно (помимо Docker Compose) — это тривиальный шелл-скрипт (который будет rsync-ом заливать на сервер каталог с docker-compose.yml, после чего выполнять на сервере через ssh docker compose up) и какой-то способ держать зашифрованные секреты в git.
Основную сложность — приведение сервера в заданное состояние из любого текущего состояния (то, ради чего обычно используют Ansible) — берёт на себя Docker Compose.
Ну, почти — нужно будет ему немного с этим делом помочь, но всё получится!

Сначала я использовал один шелл-скрипт на 20 строк плюс git-secret.
Но недавно добавил mise (исключительно для удобства), и перешёл с git-secret на fnox (для безопасного использования AI-агентов, чтобы они не получили случайно доступ к секрету, просто прочитав файл).

Конкретный пример того, как выглядит полноценная реализация этого подхода, вы можете увидеть в https://github.com/powerman/myservers-template. Это урезанный вариант моей текущей конфигурации, почищенный от личных данных и готовый для использования в качестве шаблона для создания вашего репо. Он содержит пример достаточно сложной настройки (primary и secondary сервера для собственных DNS и email сервисов, VPN для доступа в домашнюю локалку, полноценный мониторинг Netdata с алертами в телеграм). Свои DNS и email вряд ли нужны многим, но вот файрвол/мониторинг/VPN вполне могут пригодиться и для ваших серверов.
Плюс там ещё пример того, как можно вести документацию по такой инфраструктуре, с диаграммой.
Это именно пример как такое настраивается, а не готовый к выкату на ваши сервера проект!

А теперь о собранных граблях — какие проблемы не решил Docker Compose и их пришлось решать мне:

  • Начальная настройка сервера — тот же docker нужно на сервер как-то установить. Я для этой цели завёл bootstrap.sh — скрипт, который нужно однократно выполнить на новом сервере (можно передать его как cloud-init user-data script в админке облачного хостинга или запустить вручную). Я старался сделать его как можно меньше, чтобы минимизировать необходимость его изменений в будущем — потому что его изменение эквивалентно необходимости снова настраивать сервер вручную, да ещё и не забывать дублировать эти ручные изменения в bootstrap.sh. Но всё равно нашлось целых две причины его менять: обновление на следующий релиз OS (напр. через do-release-upgrade в Ubuntu), и необходимость обновить установленные для удобства моей работы в консоли сервера инструменты (напр. когда я перешёл с Vim на Neovim и мне на всех серверах понадобился мой конфиг в /root/.config/nvim).

  • Идемпотентная настройка файрвола. Учитывая, как докер сам изменяет настройки файрвола, понадобилось придумать подход, состоящий из начальных правил /etc/nftables.conf (создаваемых bootstrap.sh) и определённой структуры файла миграции этих правил migrate.nft (выполняемого внутри контейнера при docker compose up), позволяющий не затрагивать текущие правила докера и при этом вносить любые необходимые изменения.

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

  • Письма от сервисов OS (ошибки systemd, unattended-upgrades, etc.). Поскольку у меня на серверах всё равно используется postfix, то решил через bind-mount /var/spool/postfix хоста в контейнер postfix, чтобы очередь писем у них стала общая и postfix внутри контейнера доставлял в т.ч. и письма от системных сервисов.

  • Специфика конкретных серверов/OS. Пришлось для каждого сервера написать небольшой скрипт host-facts.sh, который создаёт переменные окружения, содержащие эту специфику (названия сетевых интерфейсов, IP адреса, UID/GID системных аккаунтов, etc.), которые дальше используются в docker-compose.yml.

  • HEALTHCHECK критичен и необходим — иначе при деплое не поймёшь, что сервис не запустился. Многие образы докера из коробки идут без HEALTHCHECK — приходится добавлять самому.

  • Обновления. Да, для пакетов OS обновления ставит unattended-upgrades, но при этом подходе в OS хоста практически ничего нет, все сервисы внутри контейнеров и их тоже нужно обновлять. Renovate умеет обновлять и базовые образы (строку FROM в Dockerfile), и образы в docker-compose.yml, и пакеты Alpine (RUN apk add), и даже сторонние приложения, установленные произвольным способом (вроде postgrey, скачиваемого по URL из GitHub) — через custom managers с regex. Так что придётся использовать именно его. И держать этот проект в приватном репо на гитхабе.

Ещё одна полезная штука — тестирование на CI и локально перед выкатом. Оно, конечно, не обязательно, но… хочется видеть ошибки не когда уже сервис упал при выкате, а чуть раньше.
В общем, я немного накидал в проект форматировщиков/линтеров (благо mise очень упростил их установку и запуск), плюс сделал поддержку локальной сборки образов в host-facts.sh и без необходимости расшифровки реальных секретов, но тут ещё можно много всякого добавить.

Покажу несколько немного упрощённых примеров реализации, для иллюстрации подхода.

Деплой — это rsync каталога сервера на хост + docker compose up, всё в одном скрипте:

rsync --recursive --links --delete "./$proj/" "${destination}:.myserver"

ssh -t "${destination}" 'set -euo pipefail
    cd ~/.myserver
    docker compose up --build --detach --remove-orphans --wait'

--wait тут критичен: он ждёт, пока все HEALTHCHECK-и не станут healthy (или не упадут).
Вы видите результат прямо в терминале и можете сразу откатиться: git checkout @~ (ну или git stash) + повторный деплой.
На 2-5 личных серверов автоматический деплой не нужен: важнее видеть что происходит глазами, чем экономить минуту автоматизацией.
Плюс резервирование (primary+secondary для DNS/email) покрывает окно неудачного деплоя.

Специфика хоста определяется через host-facts.sh — скрипт, который при деплое создаёт переменные окружения для docker-compose.yml:

uid() { getent passwd "$1" | cut -d: -f3; }
gid() { getent group "$1" | cut -d: -f3; }
metadata() { curl -s "http://169.254.169.254/metadata/v1$1"; }

set -euo pipefail

if test -z "${DEVEL:-}"; then
    WAN_IP="$(metadata /interfaces/public/0/ipv4/address)"
    UID_POSTFIX="$(uid postfix)"
    GID_POSTFIX="$(gid postfix)"
    # ...
else
    WAN_IP=0.0.0.0
    UID_POSTFIX=1001
    GID_POSTFIX=1001
    # ...
fi
for _var in \
    WAN_IP \
    UID_POSTFIX \
    GID_POSTFIX; do eval "export $_var=\"\$$_var\""; done

Ветка else с фоллбэками нужна для сборки образов локально и на CI — без реального железа.
Структура скрипта подобрана так, чтобы опечатки и пропущенные переменные ловились линтером или шеллом при выполнении, плюс его было очень просто проверить глазами.

Идемпотентный файрвол — отдельная история. Docker создаёт свои правила nftables в family ip и ip6 (отдельно для каждого протокола), поэтому все «свои» правила нужно держать в family inet (который покрывает и IPv4, и IPv6), чтобы не конфликтовать:

flush ruleset inet                 # Сбрасываем только свои правила.
flush chain ip filter DOCKER-USER  # Чистим цепочки для docker forward.
flush chain ip6 filter DOCKER-USER

table inet filter {
    chain prerouting-before-docker {
        type filter hook prerouting priority dstnat - 2;
        # Фильтруем ДО docker NAT — иначе docker dnat делает невозможной
        # фильтрацию по оригинальному порту назначения.
        ...
    }
}

Этот migrate.nft рендерится из шаблона через dockerize (подставляя IP-адреса из переменных) и применяется контейнером при каждом docker compose up — полностью идемпотентно.

WireGuard — ещё один нюанс: контейнер создаёт сетевой интерфейс ядра, и при остановке его нужно явно удалить, иначе повторный запуск сломается:

finish() {
    wg-quick down "$INTERFACE"
}
trap finish EXIT

wg-quick up "$INTERFACE"
sleep inf

Для существующих серверов этот подход тоже возможно применять. Да, там не получится чистой настройки «с нуля» с bootstrap.sh, и, скорее всего, не получится сразу перенести вообще всё, но можно переносить сервисы в контейнеры по одному, постепенно — лучше так, чем никак.

Ещё один плюс этого подхода — появляется очевидное место, где удобно документировать свою инфраструктуру.

В общем, подход «docker compose up как IaC» оказался вполне рабочим.
Грабли есть, но они разовые — наступил, решил, зафиксировал в конфиге.
Дальше серверы живут в git, деплой — одна команда, восстановление сервера с нуля занимает минуты, а не бесконечность.

Комментарии (10)


  1. gtosss
    25.03.2026 00:43

    Вижу что молча минусуют. Не понимаю людей — почему не написать краткий комментарий, что не так. (я не автор статьи)

    Скорее всего минусуют из-за провакционно-корявой КДПВ


    1. baldr
      25.03.2026 00:43

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

      Идемпотентность и удалённый бутстрап - это Chef/Puppet/Salt/Ansible/etc. В зависимости от масштабов. А писать свои скрипты - это несерьёзно.

      Хранить секреты в git - я этого вообще не понимаю, ну да бог вам судья, у каждого свой подход.


      1. powerman Автор
        25.03.2026 00:43

        Я и не сомневался, что так делают многие, идея вполне очевидная и напрашивающаяся. Проблема, на мой взгляд, как раз в том, что все делают это молча, хотя возникающие в процессе сложности и подходы к их решению могут быть очевидны далеко не всем. Так что статья не "о достижении", а просто чтобы задокументировать этот подход и связанные с ним проблемы/решения.

        Chef/Puppet/Salt/Ansible/etc. В зависимости от масштабов. А писать свои скрипты - это несерьёзно.

        Ну я вот пытался внедрить Ansible на протяжении многих лет. Не идёт. Слишком сложно для таких масштабов, как у меня. Поэтому заставить себя не просто почитать его доку и немного поиграться, а начать использовать полноценно у меня просто не получалось. А Ansible - это лучшее из всего упомянутого.

        Поэтому не вижу ничего плохого в одном тривиальном скрипте, который вполне соответствует масштабу задачи, и позволил мне таки перейти на IaC. Да, пытаться колхозить скрипт для решения этой задачи на тех масштабах, где уже нужно использовать Ansible - дурная идея. Но туда тащить этот подход я и не предлагал.

        Хранить секреты в git - я этого вообще не понимаю, ну да бог вам судья, у каждого свой подход.

        fnox позволяет держать секреты где угодно. Но, мне просто интересно, а где Вы предлагаете хранить секреты для пары личных серверов? И чем, на Ваш взгляд, мой вариант принципиально отличается от Ansible Vault?


        1. baldr
          25.03.2026 00:43

          Для серверов, которые нужно пересоздавать раз в 2-3 года - можно использовать что угодно, даже просто текстовый файлик со списком софта.

          Проблема со своими скриптами - и я на это уже много раз натыкался сам - в том, что через эти 2-3 года внезапно обнаруживаешь, что сами скрипты устарели. Wireguard ставится из другого репозитория.. Для установки докера в старой инструкции можно было сделать add-apt-repository , а теперь нужно в keyrings писать ещё.. ssh слушает порт через systemd вместо конфига.. И скрипты надо править каждый раз, причём обнаруживаются все эти проблемы в тот момент, когда уже надо бы срочно всё восстановить..

          В нормальном плейбуке все эти обновления уже отражены и плейбук "установи докер" установит его самым правильным на текущий момент способом.

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

          а где Вы предлагаете хранить секреты для пары личных серверов?

          Я храню в keepass. И на серверах пишу в конфиги руками если нужно.

          Мне кажется очевидным, что хранить секрет в git репозитории, даже зашифрованным, очень небезопасно. Оно же всю историю коммитов хранит. Если ключ шифрования скомпрометирован, то даже если вы его смените и перешифруете секреты - предыдущие коммиты доступны со старым ключом. Разделения доступа нет - это только для одного человека с одним ключом. Если нужно временно дать доступ только на часть секретов для коллеги - это невозможно, и отозвать доступ тоже непросто. Если репозиторий не на своём сервере, а на Github - то это вообще чужое облако.. На серверах я никогда не делаю git clone чтобы не давать доступ (даже readonly). В общем, небезопасно это всё.


          1. powerman Автор
            25.03.2026 00:43

            Проблема со своими скриптами

            Эта проблема касается только bootstrap.sh, а он минимален. Да, надо поставить докер и настроить ssh - но это плюс/минус всё, и поддерживать этот набор не сложно. Да и новые сервера в концепции "пара личных" появляются не часто. Всё это по-прежнему сильно проще, чем разбираться с Ansible (который, к слову, тоже постоянно развивается, меняется, и его конфиги скорее всего тоже через пару лет нужно адаптировать к текущей версии ансибла - так что размен такой себе).

            Я храню в keepass. И на серверах пишу в конфиги руками если нужно.

            Ну, свои (личные, а не которые для серверов) я тоже в Keepass держу. Но писать в конфиги сервера ручками - это ломает перевыкат сервера "одной кнопкой", разве нет? Как Вы этот подход с тем же Ansible совмещаете?

            Если ключ шифрования скомпрометирован, то даже если вы его смените и перешифруете секреты - предыдущие коммиты доступны со старым ключом.

            Если ключ скомпрометирован, то нужно сменить все защищённым им секреты. В любом случае, вне зависимости от того, где они хранились, и доступны ли старые коммиты - разве нет?

            Разделения доступа нет - это только для одного человека с одним ключом.

            Ну, технически fnox всё это умеет, и пригоден даже в enterprise. Но практически, для заявленной задачи "до 5 личных серверов" разделение доступа явно лишняя фича.


            1. baldr
              25.03.2026 00:43

              У меня на продакшене 5 серверов и сетап у них примерно как у вас - тоже есть bash-скрипт с начальными шагами. Но мне это очень не нравится, и мы планируем серьёзно это всё переделать.

              Повторюсь - я сам так делаю, но считаю что это неправильно и учить такому не стоит.

              Если ключ скомпрометирован, то нужно сменить все защищённым им секреты.

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

              Что касается хранения секретов - это боль. Мне не нравится ни один менеджер паролей и я уже год не спеша (времени нет) пишу свой (с картами и девушками). На продакшене секреты хранятся и используются в Docker Secrets (Swarm), а чтобы их удобнее менять - у меня самописный менеджер для Swarm их умеет подменять.


              1. powerman Автор
                25.03.2026 00:43

                Повторюсь - я сам так делаю, но считаю что это неправильно и учить такому не стоит.

                Эта позиция (в отношении других вещей) мне хорошо понятна. Более того, я примерно из этих соображений несколько раз пытался освоить Ansible, да и на все альтернативы смотрел - те, которые Вы упомянули плюс ещё Terraform. И хотя я в основном всё-таки архитект и программист, но у меня хватает и админского опыта: начинал со слаквари в 94-м, в 2001 даже сделал собственный дистрибутив линуха на базе LFS и поддерживал его года 2.5 примерно, после этого перешёл на Gentoo, которым пользуюсь до сих пор. И я прекрасно понимаю всю пользу IaC. Но если при таком бэкграунде, многолетнем желании внедрить IaC, и нескольким подходам к существующим инструментам, я этого так и не сделал - возможно дело не во мне (моей лени или безграмотности), а в том, что существующие инструменты действительно не подходят для такой задачи и целевой аудитории "не профессиональный админ"?

                Кроме того, даже если так делать неправильно, может всё-таки лучше так, чем никак (настраивать ручками без IaC)? У этого подхода довольно узкая область применимости - но я вроде бы честно и чётко её в статье обозначил. И если ни я ни Вы не взяли Ansible, а делаем так - почему "плохо" про это честно написать? От того, что про этот подход все будут стыдливо молчать - что изменится к лучшему? У меня вот управление серверами с ним однозначно стало лучше - IaC вполне рабочий получился, с IaC и документацией стало на порядок лучше, чем без них. Почему про это плохо рассказывать и подталкивать внедрять IaC хотя бы такими, колхозными методами?


                1. baldr
                  25.03.2026 00:43

                  С одной стороны, я понимаю что вы имеете в виду. Однако, "я бы мог с вами согласиться, но тогда мы бы оба оказались неправы" (с)

                  Для меня это выглядит как легитимизация неправильного подхода. И после прочтения может остаться желание оставить всё как есть: "смотри, другие так же делают". Однако, когда я объясняю что-то коллегам, я часто говорю "я сделал это вот так, но это НЕПРАВИЛЬНО, должно быть вот так, мы это должны переделать когда сможем", заводится таск в Jira и мозолит глаза.

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


        1. Tony-Sol
          25.03.2026 00:43

          Ну я вот пытался внедрить Ansible на протяжении многих лет. Не идёт. Слишком сложно для таких масштабов, как у меня.

          В статье говорится про 1-5 тачек - если не это идеальная возможность поработать с ansible, то что? Проще разве что из tmux панелей подключаться ко всем тачкам разом и через pane-synchronize один shell'ник, запускать


          1. powerman Автор
            25.03.2026 00:43

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