Лучшие практики и антипаттерны Docker

Рейтинг: 61% · 6 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 54
Зарегистрирован: 11 май 2026, 05:31

Лучшие практики и антипаттерны Docker

Сообщение Marina_DevOps »

Оглавление курса (46)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
  18. Архитектура Docker: dockerd, containerd, runc и OCI
  19. Как работает изоляция: namespaces и cgroups
  20. Образы изнутри: слои, OverlayFS и storage driver
  21. Жизненный цикл контейнера и политики перезапуска
  22. Отладка контейнеров: exec, attach, nsenter, debug
  23. Сети Docker глубже: bridge, overlay, macvlan и iptables
  24. Docker Compose глубже: profiles, healthcheck-зависимости, override
  25. Данные и тома глубже: драйверы, бэкап, права
  26. Секреты и конфигурация: как не светить пароли
  27. Ограничение ресурсов: CPU, память, OOM и cgroups
  28. Здоровье и автозапуск: HEALTHCHECK, init и сигналы
  29. Логирование контейнеров: драйверы, ротация, сбор
  30. Мониторинг контейнеров: stats, cAdvisor, Prometheus
  31. Безопасность глубже: rootless, user namespaces, seccomp, capabilities
  32. Supply chain: сканирование, SBOM и подпись образов
  33. Глубокая оптимизация образов: distroless, scratch, кэш
  34. Multi-arch и сборка под ARM: buildx и эмуляция
  35. Реестры в продакшене: Harbor и облачные registry
  36. Docker без Docker: Podman, containerd, nerdctl, Buildah
  37. От Compose к оркестрации: когда контейнеров становится много
  38. Docker Swarm: встроенная оркестрация
  39. Сборка образов в CI без демона: kaniko, BuildKit, Buildah
  40. Траблшутинг Docker: типичные ошибки и как чинить
  41. Производительность и эксплуатация Docker-хоста
  42. Docker Desktop, WSL2 и альтернативы на Windows и Mac
  43. Контейнеризация приложений: бэкенд, фронтенд, БД
  44. Docker и Kubernetes: containerd, миграция, kompose
  45. Лучшие практики и антипаттерны Docker (вы здесь)
  46. Сквозной проект: production-ready стек и путь дальше
Ты уже умеешь собирать образы, гонять контейнеры, поднимать стек через Compose. Дальше начинается то, что отличает игрушку от прода: дисциплина. Большинство ночных инцидентов с контейнерами - это не баги Docker, а нарушенные правила. Образ на 2 ГБ, который тянется по полчаса. latest, который вчера был одним билдом, а сегодня другим, и ты не понимаешь, почему сломалось. Секрет, запекшийся в слой и уехавший в реестр. Контейнер от root, через который кто-то вышел на хост. Этот урок - свод docker best practices: не список заповедей с потолка, а механика, почему каждое правило именно такое, и какие docker антипаттерны за ним стоят. Разберём docker лучшие практики по слоям: процесс, образ, секреты, рантайм - и в конце соберём чеклист код-ревью Dockerfile, который можно повесить в CI.

Один процесс на контейнер: почему не VM

Главное недопонимание новичка - воспринимать контейнер как лёгкую виртуалку, куда надо засунуть nginx, php-fpm, cron и заодно базу. Это первый и самый дорогой антипаттерн. Контейнер - это не машина, это обёртка вокруг ОДНОГО процесса. У него один PID 1, и весь жизненный цикл контейнера завязан на этот процесс: умер PID 1 - умер контейнер.

Почему один процесс? Потому что на этом держится вся модель. Оркестратор (Compose, k8s, Swarm) масштабирует, рестартует и проверяет здоровье на уровне контейнера. Если внутри пять процессов, а упал один из них (не PID 1), Docker этого не увидит - контейнер живой, сервис мёртвый. Логи смешиваются в кашу. Обновить один компонент нельзя, не пересобрав всё. Масштабировать nginx отдельно от php нельзя - они склеены.

Правильно - разнести по контейнерам и связать сетью: nginx, php-fpm, postgres, redis - каждый своим. Это и есть docker правила в чистом виде. Если внутри одного контейнера действительно нужно несколько процессов (редкий случай - например, приложение плюс sidecar-агент в одном образе), бери легковесный init вроде tini или dumb-init, а не полноценный systemd. Кстати, поэтому в exec-форме ENTRYPOINT так важен правильный PID 1: без init зомби-процессы не реапятся, а сигналы (SIGTERM при docker stop) не доходят до приложения, и контейнер умирает по таймауту через SIGKILL.

Изображение

Иммутабельность и теги: не latest в проде

Контейнер - вещь одноразовая. Правило: не правь работающий контейнер, пересобирай образ. Зашёл через docker exec, поставил пакет, поправил конфиг - поздравляю, ты создал снежинку, которую невозможно воспроизвести. Перезапустишь контейнер - изменения исчезнут (writable-слой стирается). Раскатишь на второй ноде - там этих правок нет. Иммутабельность означает: образ - это артефакт, его собирают раз и катают везде одинаковым. Все изменения - только через Dockerfile и пересборку.

Отсюда вырастает тема тегов. latest в проде - антипаттерн номер один по последствиям. latest - это не "последняя стабильная версия", это просто тег по умолчанию, обычная мутабельная метка. Сегодня nginx:latest указывает на один билд, завтра мейнтейнер перетегировал - и тот же docker pull притащит другой образ. Воспроизводимость сборки умерла, а ты узнаешь об этом в три ночи.

Уровни надёжности по возрастанию:
  • latest - мутабельный, для прода запрещён;
  • семантический тег вроде 1.27 или 1.27.4 - лучше, но мейнтейнер может перезалить и его;
  • pin по digest - единственная по-настоящему иммутабельная ссылка.
Digest - это SHA256 от содержимого образа (content-addressable). Он не врёт: если контент изменился, изменился и хеш. В Dockerfile это выглядит так.

Код: Выделить всё

# Мутабельно - так в проде нельзя
FROM node:22-alpine

# Иммутабельно - тег для читаемости плюс digest для гарантии
FROM node:22-alpine@sha256:7d7f3...e1b9
Посмотреть digest можно так.

Код: Выделить всё

docker buildx imagetools inspect node:22-alpine
# или у уже подтянутого образа:
docker inspect --format='{{index .RepoDigests 0}}' node:22-alpine
# node@sha256:7d7f3a...e1b9
В 2026 это часть supply chain гигиены: теги мутабельны и могут тихо измениться между сборками (supply chain drift), поэтому каждую базовую ссылку в каждом Dockerfile пинят по digest. Обновлять digest вручную больно, поэтому это вешают на Renovate или Dependabot - они открывают PR с новым хешем, ты ревьюишь и мёрджишь.

Образ: минимальный, кэш-дружелюбный, без лишнего

Fat-образ (жирный) - классический антипаттерн. Тащить в прод полный ubuntu с компилятором, заголовками и dev-зависимостями - это медленный pull, лишние гигабайты в реестре и огромная поверхность атаки: каждый пакет - потенциальная CVE. Лекарство - минимальная база и multi-stage build. В стадии сборки у тебя есть всё (компилятор, dev-deps), а в финальный образ копируется только артефакт.

Код: Выделить всё

# Стадия сборки - тут можно всё
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

# Финал - только бинарь, ничего лишнего
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot
ENTRYPOINT ["/server"]
Multi-stage реально срезает образ с гигабайта до десятков мегабайт. Для статических бинарей - distroless или вообще scratch (пустой образ, ноль пакетов, ноль CVE в базе). Для интерпретируемых языков - alpine или slim-варианты, но осторожно: на alpine musl libc вместо glibc, и нативные расширения иногда ломаются - проверяй.

Теперь dockerfile best practices про КЭШ. BuildKit (по умолчанию в современном Docker) кэширует слои, и порядок инструкций определяет, сколько кэша переживёт изменение кода. Правило: от редко меняющегося к часто меняющемуся. Сначала ставим зависимости (меняются редко), потом копируем исходники (меняются каждый коммит).

Код: Выделить всё

# ПЛОХО - любое изменение кода рушит кэш установки зависимостей
COPY . .
RUN npm ci

# ХОРОШО - манифесты отдельно, кэш npm ci переживает правки кода
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Разница огромная: в первом случае правка одной буквы в коде заставляет переустанавливать ВСЕ зависимости (минуты), во втором - npm ci берётся из кэша (секунды). BuildKit добавляет cache mount - кэш менеджера пакетов переживает даже изменение манифеста.

Код: Выделить всё

RUN --mount=type=cache,target=/root/.npm npm ci
И обязательный .dockerignore рядом с Dockerfile. Без него COPY . . затягивает в контекст сборки .git, node_modules, .env, логи - всё уезжает в слои, раздувает образ и сливает секреты.

Код: Выделить всё

.git
node_modules
*.log
.env
.env.*
Dockerfile
Не от root, секреты снаружи, логи в stdout

Контейнер от root - антипаттерн с прямым риском. По умолчанию процесс в контейнере - root, и хоть namespaces его изолируют, при любой уязвимости рантайма или мисконфиге (а особенно при примонтированном docker.sock или --privileged) это становится root на хосте. Правило: добавь непривилегированного пользователя и переключись на него. Бонус - ты сразу ловишь приложения, которые зачем-то лезут писать в системные каталоги.

Код: Выделить всё

RUN addgroup -S app && adduser -S app -G app
USER app
Rootless-режим идёт ещё дальше: сам демон Docker и контейнеры работают под обычным пользователем, и root-овый dockerd как поверхность атаки исчезает в принципе. Для CI и многопользовательских хостов в 2026 это уже норма, не экзотика.

Секреты в образе или в ENV - грубейшая ошибка. Два частых способа налажать:
  • ARG/ENV с паролем - значения ENV видны любому через docker inspect и попадают в дочерние процессы;
  • COPY ключа с последующим RM в другом слое - файл остаётся в нижнем слое истории, его достаёт docker history и распаковка образа.
Секрет нельзя "удалить" из слоя поздним RUN rm - слои аддитивны, удаление лишь маскирует. Правильно - секреты снаружи: переменные окружения из секрет-менеджера в рантайме (docker secret в Swarm, Secrets в k8s, Vault), а на этапе сборки - BuildKit secret mount, который монтирует файл только на время RUN и не пишет его ни в один слой.

Код: Выделить всё

RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

Код: Выделить всё

docker build --secret id=npm_token,src=./npm_token.txt .
Логи - только в stdout/stderr. Не пиши логи в файл внутри контейнера: контейнер эфемерен, файл умрёт вместе с ним, и логи раздуют writable-слой. Двенадцатифакторный подход: приложение шлёт всё в stdout, а сбором занимается logging driver Docker (json-file, journald, fluentd, к loki/ELK). Так логи переживают контейнер и собираются централизованно. И не забывай ротацию json-file - дефолтный драйвер без лимита забьёт диск.

Healthcheck, сигналы и грабли рантайма

HEALTHCHECK даёт Docker и оркестратору правду о состоянии приложения. Без него "контейнер запущен" означает лишь "PID 1 жив", а не "сервис отвечает". Healthcheck бьёт по реальному эндпоинту.

Код: Выделить всё

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:8080/health || exit 1
start-period - грейс на старт приложения (в это время фейлы не считаются), retries - сколько подряд провалов до статуса unhealthy. В docker ps статус виден в скобках.

Код: Выделить всё

docker ps
# CONTAINER ID   IMAGE      STATUS                    PORTS
# a1b2c3d4e5f6   api:1.4    Up 2 minutes (healthy)    0.0.0.0:8080->8080/tcp
Видишь (health: starting), потом (healthy) или (unhealthy) - и оркестратор может не слать трафик на нездоровый или рестартить его.

Сигналы - частая боль. docker stop шлёт PID 1 SIGTERM и ждёт grace-period (по умолчанию 10 с), потом добивает SIGKILL. Если приложение не PID 1 (запущено через shell-форму CMD, обёрнуто в sh -c), SIGTERM уходит шеллу, а не приложению - graceful shutdown не срабатывает, соединения рвутся, контейнер тупит 10 секунд и умирает жёстко. Поэтому exec-форма обязательна.

Код: Выделить всё

# shell-форма - PID 1 это /bin/sh, сигналы не доходят
CMD npm start

# exec-форма - приложение само PID 1, ловит SIGTERM
CMD ["node", "server.js"]
Рантайм-антипаттерны: docker.sock, privileged, данные в образе

Три способа выстрелить себе в ногу на запуске.

Монтирование docker.sock в контейнер. /var/run/docker.sock - это фактически root на хосте: контейнер с ним может запускать привилегированные контейнеры, лезть в другие и сбежать на хост. Часто так делают ради "контейнера, который управляет контейнерами" (CI-агенты, watchtower). Если без этого никак - проксируй сокет через ограничивающий прокси, давай read-only и минимум прав, а лучше используй rootless или сокет-активацию.

--privileged без нужды. Флаг выдаёт контейнеру ВСЕ capabilities ядра, доступ к устройствам и снимает защиту. Нужен он реально редко. Если требуется одна-две привилегии - выдавай точечно через --cap-add (например, NET_ADMIN), а остальное лучше сразу срезать.

Код: Выделить всё

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
  --read-only --security-opt=no-new-privileges myapp:1.4
Данные внутри образа. База, загрузки пользователей, кэш - не место в образе. Образ иммутабелен и эфемерен: пересоберёшь или пересоздашь контейнер - данные исчезнут. Состояние - в именованные тома или bind-mount, чётко отделяя stateless-код от stateful-данных.

И верхний слой supply chain: собранный образ надо сканировать и подписывать. Trivy или Docker Scout гоняют по CVE и генерят SBOM (опись содержимого), cosign (Sigstore, keyless через Fulcio/Rekor) подписывает, а политика в кластере не пускает неподписанное или с критическими дырами. SBOM при этом хранят рядом с образом, а не выбрасывают.

Код: Выделить всё

trivy image --severity HIGH,CRITICAL myapp:1.4
cosign sign myregistry.ru/myapp@sha256:7d7f3...e1b9
RU-ремарка: Docker Hub из РФ временами недоступен, поэтому базовые образы держи в Yandex Container Registry или VK Cloud Registry (или своём Harbor) и пинь по digest оттуда - заодно и supply chain под контролем.

Мини-лаба: переписать плохой Dockerfile

Возьми заведомо плохой Dockerfile и почини его по чеклисту.

Код: Выделить всё

# BAD
FROM node:latest
COPY . .
RUN npm install
ENV DB_PASSWORD=secret123
CMD npm start
Задание руками:
  • Замени node:latest на конкретный тег плюс digest (docker buildx imagetools inspect node:22-alpine, подставь хеш).
  • Сделай multi-stage: deps-стадия с npm ci, финал на slim/alpine, скопируй только нужное.
  • Поправь порядок слоёв: сначала COPY package*.json и npm ci, потом COPY . .
  • Добавь .dockerignore с node_modules, .git, .env.
  • Убери ENV DB_PASSWORD - пароль пробрось в рантайме через docker run --env-file или секрет.
  • Добавь непривилегированного USER, exec-форму CMD ["node","server.js"] и HEALTHCHECK.
  • Собери, потом docker history myapp - убедись, что секрета в слоях нет, а docker ps показывает (healthy).
Чеклист код-ревью Dockerfile

Этот список - то, что должно гореть красным на ревью:
  • База пинится по digest, не latest;
  • multi-stage, финальный образ минимальный (distroless/alpine/slim/scratch);
  • слои упорядочены кэш-дружелюбно (зависимости до кода);
  • есть .dockerignore, в контекст не утекает лишнее;
  • нет секретов в ARG/ENV/слоях; сборочные секреты через --mount=type=secret;
  • процесс не от root, есть USER, по возможности read-only и no-new-privileges;
  • CMD/ENTRYPOINT в exec-форме, корректный PID 1 и обработка SIGTERM;
  • есть HEALTHCHECK по реальному эндпоинту;
  • логи в stdout/stderr, состояние в томах, не в образе;
  • образ сканируется (Trivy/Scout), есть SBOM, релизный образ подписан.
Итог

Docker лучшие практики - это не про красоту, а про воспроизводимость и безопасность под нагрузкой. Один процесс на контейнер, иммутабельность с пином по digest вместо latest, минимальный multi-stage образ с кэш-дружелюбным порядком слоёв, не от root, секреты снаружи, логи в stdout, healthcheck с корректными сигналами. А антипаттерны - fat-образы, секреты в слоях, данные в образе, docker.sock и --privileged без нужды - убирай на ревью по чеклисту, пока они не убрали тебя в проде.
👍2 ❤️3 🔥2 😄 🤔1
Аватара пользователя
vault3
Сообщения: 1
Зарегистрирован: 11 май 2026, 15:33

Re: Лучшие практики и антипаттерны Docker

Сообщение vault3 »

Дошло наконец, почему latest нельзя в прод - думал это просто стиль, а это мутабельная метка которая молча подменяется. Перевёл базовые образы на digest через Renovate.
👍 ❤️1 🔥1 😄 🤔
Аватара пользователя
pythondev
Сообщения: 2
Зарегистрирован: 26 май 2026, 03:28

Re: Лучшие практики и антипаттерны Docker

Сообщение pythondev »

А если приложение само пишет логи в файл и переписать его нельзя - что делать? Слинковать файл на /dev/stdout прокатывает или костыль?
👍 ❤️1 🔥 😄 🤔2
Ответить
← Предыдущая глава
Docker и Kubernetes: containerd, миграция, kompose
Следующая глава →
Сквозной проект: production-ready стек и путь дальше

Все главы курса «Docker: контейнеризация от основ до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость