Преимущества¶
В большой команде разработчиков, где есть текучка удобно использовать готовые инструменты, а не обучать каждого джуна как твоими скриптами пользоваться. И если мы уходим с локалхоста, то есть еще k8s который дает возможность легко масштабировать приложение и распределять нагрузку (конечно во все проекты тянуть его не стоит и руки прямые нужны).
Почему docker?
- Воспроизводимость окружения
Образ — это иммутабельный артефакт: те же бинарники, те же системные либы, тот же glibc/musl, что ты тестировал. На bare metal ты рано или поздно ловишь "на проде другая версия libssl / другой systemd / кто-то руками поправил конфиг полгода назад". Образ убирает дрейф конфигурации между нодами и между stage/prod.
Каждая инструкция = immutable слой с хешем (sha256 от содержимого). Финальный образ — это стопка слоёв + манифест. Ключевое:
Образ адресуется по digest: sso@sha256:abc123.... Этот digest гарантирует, что байт-в-байт тот же образ, что ты собрал и протестировал, поедет на прод. Не «версия 1.4», а конкретные биты. На прод НЕ едет компилятор, исходники, go-кэш — только финальный слой с бинарником. Multi-stage build выбрасывает всё лишнее. distroless/scratch база: в образе вообще нет ОС в привычном смысле — нет bash, нет apt, иногда нет даже libc. Только твой статический Go-бинарник + сертификаты. Образ 10-20 МБ. Почему это «воспроизводимость», а bare metal — нет: На bare metal твой бинарник линкуется/обращается к тому, что лежит на машине: /lib/x86_64-linux-gnu/libssl.so, версия которой зависит от того, когда машину последний раз apt upgrade-нули. Две ноды, поднятые с разницей в полгода, — это два разных окружения. Контейнер таскает свои зависимости с собой, ОС-хоста почти не касается (нужен только совместимый kernel).
Нюанс честности: сам Docker build не on-100% детерминирован (apt-get install притянет свежие пакеты в разные дни). Полная воспроизводимость билда — это уже Nix или pinned digest базовых образов + lock-файлы. Но на рантайме воспроизводимость абсолютная: один digest = одно поведение.
- Атомарный деплой и откат
pull новый тег -> старый контейнер стоп -> новый старт. Откат = запустить предыдущий тег. На bare metal откат бинарника + его зависимостей + конфига — это руками или сложный Ansible-плейбук с состоянием.
Bare metal откат обычно значит: выкатить старый бинарник, откатить миграции/конфиги/системные пакеты, которые ты заодно обновил, надеяться что состояние машины то же. Это процедура с состоянием и риском.
Docker откат = образы иммутабельны и хранятся в registry по тегам/дайджестам:
v42 (текущий, сломанный) → откат → v41 docker stop sso && docker run ...sso:v41 Ты не «собираешь обратно» — старый артефакт уже существует целиком в registry. Это смена указателя на immutable-артефакт. С оркестратором (k8s) это вообще kubectl rollout undo — одна команда, и он сам поднимает старые поды, дождавшись healthcheck, прежде чем убить новые.
Ключ: деплой и откат симметричны и не имеют побочных эффектов на хост. Контейнер ушёл — на ноде не осталось следов (кроме слоёв в кэше).
- Изоляция зависимостей
Два сервиса, которым нужны разные версии одной системной библиотеки/рантайма, на одной ноде. На bare metal — конфликт или городить chroot/разные пользователи руками.
Docker не виртуализирует железо. Он использует фичи ядра Linux:
Namespaces (что процесс «видит»):
mnt — своя файловая система (свой /, свои либы) pid — свои процессы (внутри контейнера твой процесс — PID 1, чужих не видит) net — свой сетевой стек (свои интерфейсы, порты, iptables) uts — свой hostname ipc, user — изоляция IPC и маппинг UID cgroups (что процесс «может потреблять»):
лимит CPU (--cpus 2), памяти (--memory 512m), IO при превышении памяти — OOM-kill только этого контейнера, не всей ноды Что это даёт на практике:
Сервис A нужен libicu 66, сервис B — libicu 70. На одной ноде. В контейнерах — без конфликтов, каждый со своим набором либ в своём mnt namespace. Python-сервис с venv/системными пакетами и Go-сервис рядом — не дерутся за глобальные пакеты ОС. «Сосед» сожрал память → cgroup его прибьёт, остальные живы. На bare metal один сервис с утечкой может утянуть всю ноду в swap-смерть. Это та же технология, что используется напрямую (это clone() + cgroups syscalls) — Docker просто удобная обёртка над ними.
- Единый интерфейс эксплуатации
Логи (stdout->ваш Loki), рестарт-политики, лимиты CPU/RAM (cgroups), healthcheck — одинаково для всех сервисов независимо от языка. C Grafana/Loki/Alertmanager — контейнеры с этим стыкуются из коробки.
- Локальная разработка = прод
docker compose up поднимает весь стек (SSO + Postgres + observability) идентично проду. Это сокращает у меня локально работает.
- Как поднимается окружение — декларативно
Новый разработчик: git clone && docker compose up → через минуту у него локально точная копия прод-топологии: SSO + БД + Loki + Grafana, связанные сетью, с теми же образами. Никаких «установи Postgres 16, потом Go, потом…» на три страницы README. Окружение — это код в репозитории, а не инструкция в вики.
В проде то же описание превращается в k8s-манифесты/Nomad-джобы — оркестратор читает декларацию «хочу 3 реплики sso» и приводит реальность к ней.
- Производительность — где оверхед, а где его нет
Здесь важно убить миф. Docker ≠ VM.
CPU/RAM: оверхед ~0%. Это нативные процессы хоста, просто в namespaces. Нет гипервизора, нет гостевой ОС. ps aux на хосте покажет твой процесс напрямую. Диск: overlayFS даёт небольшой оверхед на запись в слои контейнера. Поэтому данные (БД) кладут в volumes (прямой bind на хостовый диск, без overlay) — там скорость нативная. Сеть: bridge-сеть с NAT даёт небольшой latency-оверхед. Для большинства сервисов незаметно; для экстремального latency используют --network host (контейнер на сетевом стеке хоста, оверхед ноль). Вывод: для подавляющего большинства сервисов оверхед в пределах погрешности. Реальная цена — не CPU, а дополнительный слой абстракции для отладки (нужно уметь docker exec, читать overlayFS, понимать сети).
- Отказоустойчивость — главный аргумент, и он про оркестратор
Сам по себе Docker даёт базу:
restart policies: --restart=always — процесс упал, демон поднял заново. healthcheck: контейнер сам сообщает «я живой/мёртвый», и это видно в docker ps. Но настоящая отказоустойчивость появляется, когда поверх контейнеров встаёт оркестратор (Kubernetes / Nomad / Swarm), и именно ради этого все и идут в Docker:
Self-healing: нода умерла → оркестратор перезапускает её поды на других нодах. Под не прошёл healthcheck → его убивают и поднимают новый. Декларативное желаемое состояние: ты говоришь «хочу 3 реплики SSO всегда». Оркестратор постоянно сверяет факт с желаемым и чинит расхождения. Это reconciliation loop — основа всей надёжности. Rolling updates без даунтайма: новые поды поднимаются, проходят healthcheck, трафик переключается, старые гасятся. Если новый не взлетел — автоматический rollback. Горизонтальное масштабирование: нагрузка выросла → replicas: 3 → 10, оркестратор раскидал по нодам, балансировщик подхватил. На bare metal это ручная установка сервиса на новые машины. Bin-packing: оркестратор сам решает, на какую ноду поставить контейнер по свободным CPU/RAM. Утилизация железа выше. Контейнер здесь — это стандартная единица планирования. Оркестратор может делать всё вышеперечисленное именно потому, что любой сервис упакован одинаково (запустить = docker run, проверить = healthcheck, убить = stop). Без унифицированной упаковки эта автоматика невозможна.
Где Docker — оверхед, и bare metal честнее
- Stateful базы данных (Postgres). Здесь контейнеризация даёт мало, а добавляет вопросы с volume'ами, fsync, производительностью диска и бэкапами. Многие осознанно держат БД на bare metal / managed, а в контейнерах — только stateless-сервисы.
- Один сервис на одной выделенной машине без соседей и без потребности в изоляции. Тогда systemd unit + статический бинарник Go — это проще, быстрее и меньше слоёв для отладки. Go как раз идеален для bare metal: один статический бинарник без рантайма.
- Hard-realtime / выжимание latency, прямой доступ к железу, специфичные сетевые штуки. Сетевой/storage оверхед контейнеров тут мешает.
- Маленькая инфра, где некому поддерживать оркестрацию. Docker без оркестратора (Swarm/k8s/Nomad) на нескольких нодах быстро превращается в ручное месиво — и тогда выигрыша над Ansible+systemd почти нет.
Для внутренних сервисов аргумент за Docker даже сильнее, чем для публичных: внутренних сервисов обычно много и они разнородные (разные языки, разные зависимости). Единый способ запускать/мониторить/обновлять всё это — основная ценность. Альтернатива (каждый сервис со своим способом деплоя) не масштабируется по людям.
Исключение то же — stateful (БД, очереди с диском, хранилища): их часто оставляют вне контейнеров или дают им managed-решение.
Важная честность про «изоляцию»
Docker — это не граница безопасности уровня VM. Это namespaces + cgroups, общий kernel. Для изоляции окружений — отлично. Для изоляции недоверенного кода / multi-tenant с разными уровнями доверия — нужен gVisor, Kata, или VM. Не продавай Docker как security-фичу, это его слабое место.
Сведём к экономике, а не к моде:
Стандартизация единицы деплоя. Любой сервис на любом языке = один и тот же интерфейс: образ, порт, env, healthcheck, логи в stdout, лимиты через cgroups. Эксплуатация 50 разнородных сервисов становится возможной для небольшой команды. Это главное. Контейнер — общий язык всей экосистемы. CI/CD (GitLab CI, GitHub Actions), registry, оркестраторы, service mesh, мониторинг — всё построено вокруг OCI-образа как артефакта. Выбирая Docker, ты получаешь доступ ко всему этому tooling бесплатно. Dev = CI = Prod. Один и тот же образ собирается в CI, тестируется, и тот же digest едет в прод. Исчезает класс багов «в CI прошло, на проде упало из-за окружения». Отчуждаемость от человека и от конкретной машины. Знание о том, как запустить сервис, лежит в Dockerfile в репо, а не в голове админа и не в накопленном за годы состоянии конкретного сервера (snowflake server). Машины становятся «скотом, а не питомцами» — любую можно пересоздать с нуля. Иммутабельная инфраструктура. Не патчим запущенное — пересобираем образ и переезжаем. Это убирает дрейф конфигурации как класс проблемы.