Сквозной проект: production-ready стек и путь дальше

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

Сквозной проект: production-ready стек и путь дальше

Сообщение 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 стек и путь дальше (вы здесь)
Зачем нужен сквозной docker проект и какую боль он закрывает

Ты прошел весь курс: образы, слои, сети, тома, BuildKit, безопасность, supply chain. По отдельности все понятно. Но в реальности боль не в одной команде, а в том, что все это надо собрать вместе и чтобы оно не падало в три часа ночи. Поэтому финальный docker проект - не очередной "hello world", а полноценный docker стек уровня прода: API, база, кэш, reverse-proxy и фоновый воркер. Мы соберем его в одном compose.yaml и пройдемся по тем местам, где обычно ломается реальный docker production: тощие multi-stage образы, запуск без root, healthcheck с правильными зависимостями, лимиты ресурсов, секреты снаружи репозитория, ротация логов и базовый мониторинг через cAdvisor. Дальше - сборка в CI со сканом и подписью, пуш в registry, полный чеклист прод-готовности и куда расти после Docker.

Главная мысль урока: production - это не "запустилось на ноуте", а "переживает падение зависимости, рестарт хоста, утечку памяти в одном сервисе и при этом не утаскивает за собой остальные". Compose с правильными настройками держит 99%+ аптайма для одного хоста - этого хватает огромному числу проектов, а Kubernetes нужен далеко не всем и не сразу.

Изображение

Архитектура стека: пять сервисов и почему именно так

Наш docker compose проект состоит из пяти контейнеров. proxy (nginx или Caddy) - единственная точка входа, терминирует TLS и роутит трафик. api - наше приложение, stateless, его можно масштабировать репликами. worker - тот же образ, что и api, но запускает фоновую обработку очереди, а не HTTP-сервер. db (PostgreSQL) - stateful, данные живут в именованном томе. cache (Redis) - кэш и брокер задач для воркера.

Ключевой принцип: разделяй по ответственности и по жизненному циклу. API и worker - одинаковый код, но разные команды запуска и разные лимиты, потому что у них разный профиль нагрузки. База и кэш - снизу, как фундамент, и стартовать они должны раньше тех, кто к ним лезет. Это и есть граф зависимостей, который мы дальше зашьем в depends_on.

Образ: multi-stage и непривилегированный запуск

Образ для api и worker собираем в две стадии. Первая стадия тянет компилятор/зависимости и собирает артефакт, вторая копирует только готовое в тонкий runtime. Так в финальный образ не попадают ни компилятор, ни кэш пакетного менеджера, ни заголовочные файлы. Размер падает в разы, поверхность атаки - тоже.

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

# syntax=docker/dockerfile:1
FROM golang:1.24 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
Разбор того, что тут важно. --mount=type=cache - это кэш BuildKit, он переживает между сборками и не раздувает слои, в отличие от старого трюка с volume. distroless ... nonroot - базовый образ без shell и пакетного менеджера, внутри уже заведен непривилегированный пользователь. USER nonroot - процесс не стартует от root, и даже если кто-то пробьет приложение, он окажется в контейнере без оболочки и без uid 0. Флаги -s -w выкидывают отладочную информацию, -trimpath убирает абсолютные пути сборки. Если язык интерпретируемый (PHP, Python, Node) - принцип тот же: на финальной стадии бери slim-базу, копируй vendor/зависимости отдельным слоем, явно создавай пользователя и переключайся на него через USER.

Проверь, что внутри реально не root:

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

$ docker run --rm myapp:latest id
uid=65532(nonroot) gid=65532(nonroot) groups=65532(nonroot)
Если тут uid=0(root) - ты не закрыл главную дыру, возвращайся к Dockerfile.

Compose-файл: healthcheck, зависимости и лимиты

Теперь собираем docker стек. Compose v2, файл называется compose.yaml, поле version: давно не нужно и игнорируется. Покажу опорные куски, а не весь файл целиком - чтобы видна была механика.

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

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits: { cpus: "1.5", memory: 512M }
    restart: unless-stopped
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }

  api:
    image: registry.example.ru/app:${TAG:-latest}
    depends_on:
      db: { condition: service_healthy }
      cache: { condition: service_healthy }
    healthcheck:
      test: ["CMD", "/app", "healthcheck"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 15s
    deploy:
      resources:
        limits: { cpus: "1.0", memory: 256M }
    restart: unless-stopped
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }
Главные грабли новичков спрятаны в depends_on. В старом Compose v1 depends_on только ждал, пока контейнер стартует - то есть процесс запущен, но Postgres внутри еще читает WAL и не принимает соединений. API дергался к мертвой базе и падал в гонке. В Compose v2 появилось condition: service_healthy: API не запустится, пока healthcheck базы не вернет healthy. Вот почему healthcheck у зависимостей обязателен - без него condition: service_healthy просто нечему проверять.

Разбор полей healthcheck. interval - как часто пинговать (10s - разумный дефолт). timeout - сколько ждать ответа, после чего попытка считается провальной. retries - сколько провалов подряд переводят контейнер в unhealthy. start_period - окно прогрева: провалы в это время не считаются, чтобы медленный старт базы не уронил весь стек. Считай так: реальное время до пометки unhealthy это примерно start_period плюс interval умножить на retries.

Про лимиты. Без limits один сервис с утечкой памяти сожрет всю RAM хоста и OOM-killer прибьет случайные контейнеры, возможно базу. С memory: 256M ядро прибьет именно зарвавшийся контейнер (код выхода 137), restart: unless-stopped его поднимет, остальные живы. Важная тонкость: deploy.resources.limits в standalone Compose v2 работает и без Swarm - это давняя путаница из доков по Swarm. cpus: "1.0" это не одно ядро жестко, а квота в одно ядро-эквивалент через cgroups (cpu.max в cgroup v2 под /sys/fs/cgroup).

Про ротацию логов. Драйвер json-file по умолчанию пишет в /var/lib/docker/containers/.../*-json.log без ограничения. Болтливый сервис за неделю забивает диск под ноль, и хост встает. max-size и max-file включают ротацию: три файла по 10 МБ, итого максимум 30 МБ на контейнер. Это можно задать и глобально в /etc/docker/daemon.json, чтобы не дублировать в каждом сервисе.

Секреты снаружи: ничего чувствительного в образе и в git

Пароль базы в нашем примере приходит не из environment, а из секрета. Compose монтирует файл в /run/secrets/db_password (это tmpfs, в памяти, не на диске), а Postgres читает его через POSTGRES_PASSWORD_FILE. Почему не просто environment? Переменные окружения видны в docker inspect, в /proc/PID/environ, текут в логи и в дочерние процессы. Файл-секрет этих утечек лишен.

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

secrets:
  db_password:
    file: ./secrets/db_password.txt   # для одного хоста
    # либо external: true и подача через свой секрет-провайдер
Каталог secrets/ - в .gitignore, в репозиторий едет только .env.example с пустыми значениями. Это минимум. Для серьезного прода секреты живут во внешнем менеджере (HashiCorp Vault, облачный Secrets Manager, в RU-реалиях - Yandex Lockbox), а в стек подаются провайдером. Главный закон: секрет не должен попасть ни в слой образа (он навсегда останется в истории слоев, даже если удалить файл следующей инструкцией), ни в git, ни в открытую переменную.

Мониторинг: cAdvisor и почему он нужен

Лимиты ты поставил, но как понять, упирается ли сервис в потолок? Добавляем в стек cAdvisor от Google - он читает cgroups и отдает по-контейнерные метрики CPU, памяти, сети, диска.

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

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports: ["8080:8080"]
    restart: unless-stopped
cAdvisor сам по себе дает живой веб-интерфейс, но в проде его обычно скрейпит Prometheus, а дашборды рисует Grafana. Связка cAdvisor + Prometheus + Grafana - классический стартовый стек наблюдаемости для одного хоста. Когда видишь, что memory сервиса месяцами ползет вверх и упирается в лимит с рестартами - это утечка, и cAdvisor покажет ее раньше, чем пользователи.

Сборка в CI: скан, подпись, пуш в registry

Локально собрать образ умеет каждый. Production-конвейер другой: сборка воспроизводимая, образ просканирован, подписан и снабжен SBOM. Современный пайплайн идет по стадиям Build -> Scan -> Sign -> Attest -> Push.

Сборка кросс-платформенного образа через buildx с генерацией provenance и SBOM прямо в один проход:

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

$ docker buildx build \
    --platform linux/amd64,linux/arm64 \
    --provenance=true --sbom=true \
    -t registry.example.ru/app:$TAG \
    --push .
--provenance=true прикрепляет к образу аттестацию о том, как он собран (SLSA-происхождение), --sbom=true - список всех компонентов внутри. Обе аттестации едут в registry рядом с образом как OCI-артефакты. Дальше - скан и подпись:

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

$ trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.ru/app:$TAG
$ cosign sign --yes registry.example.ru/app:$TAG
trivy --exit-code 1 валит сборку, если нашлись уязвимости уровня HIGH или CRITICAL - то есть дырявый образ физически не уедет в прод. Альтернатива встроена в Docker - docker scout cves. cosign sign подписывает образ (keyless через Sigstore по OIDC-идентичности раннера, без хранения приватных ключей), а на деплое cosign verify проверяет подпись. Так в кластер не проедет образ, собранный мимо твоего пайплайна. Практическая грабля 2026: OCI referrers API на части реестров (включая историю с Docker Hub) бывает капризен с аттестациями - запасной путь хранить provenance/SBOM в GitHub Attestations, а не пушить в реестр.

RU-реалии: Docker Hub из России отдает rate limit и иногда недоступен без зеркала, поэтому базовые образы тяни через зеркало или зеркаль в свой Yandex Container Registry / VK Cloud Container Registry, а свои образы пушь туда же. В CI это просто другой адрес в -t и docker login на нужный реестр.

Чеклист прод-готовности целиком
  • Образ: multi-stage, тонкая база (distroless/slim/alpine), .dockerignore настроен, тег зафиксирован по дайджесту, не root внутри (проверено через id).
  • Безопасность: USER не root, read_only: true где можно, cap_drop: [ALL] и точечный cap_add, security_opt: no-new-privileges, секреты только файлами/из менеджера.
  • Надежность: healthcheck у каждого долгоживущего сервиса, depends_on с condition: service_healthy, restart: unless-stopped, корректная обработка SIGTERM (graceful shutdown).
  • Ресурсы: limits по cpus и memory у всех, ротация логов (max-size/max-file), тома для stateful-данных, бэкап тома базы.
  • Поставка: сборка в CI, скан Trivy/Scout с порогом по severity, подпись cosign, SBOM и provenance, пуш в приватный/зеркальный registry.
  • Наблюдаемость: cAdvisor (или экспортер) + Prometheus + Grafana, централизованные логи, алерты на unhealthy и на упор в лимит памяти.
Пробегись по нему перед каждым релизом. Большинство ночных инцидентов - это галочка из этого списка, которую "потом поставлю".

Типичные грабли и антипаттерны
  • depends_on без healthcheck у зависимости - condition: service_healthy молча деградирует, гонка возвращается.
  • latest в проде - неповторяемый деплой, "у меня вчера работало". Фиксируй тег или дайджест sha256.
  • Секрет в ARG/ENV или в COPY - он навсегда в слое истории, docker history его покажет даже после удаления.
  • Нет лимитов - один утекший сервис кладет всю машину через OOM-killer.
  • Логи без ротации - диск под ноль за неделю, классика падения хоста.
  • Healthcheck, который всегда healthy (curl на /, который отвечает даже у мертвого приложения) - оркестратор думает, что все живо, а оно нет. Проверяй реальную готовность: коннект к базе, а не просто живой порт.
Куда расти после Docker

Один хост и Compose тебя проведут далеко, но рано или поздно упрешься в потолок. Kubernetes - когда нужны несколько узлов, автоскейл, self-healing на уровне кластера и декларативные раскатки. Кстати, k8s давно выпилил dockershim и работает с containerd напрямую через CRI - Docker как демон ему не нужен, но образы остались OCI-совместимыми, так что твои сборки едут в k8s без изменений. Podman + Quadlet - демонless и rootless по дизайну, контейнеры описываются как systemd-юниты, отличный путь для одиночного хоста под управлением systemd. eBPF-наблюдаемость (Cilium/Hubble, Falco на eBPF, Pixie) - видеть и фильтровать сетевой трафик и системные вызовы контейнеров на уровне ядра без сайдкаров. Платформенная инженерия - когда сервисов и команд много, поверх всего этого строят internal developer platform, чтобы разработчик катил сервис по golden path, не зная деталей.

Мини-лаба: собери стек руками
  • Напиши multi-stage Dockerfile для своего api, добавь USER non-root, проверь docker run --rm img id - убедись, что не uid 0.
  • Собери compose.yaml из db (postgres), cache (redis), api и worker. Базе и кэшу пропиши healthcheck, у api поставь depends_on с condition: service_healthy.
  • Останови специально db (docker compose stop db) и подними стек заново - убедись, что api не стартует, пока база unhealthy.
  • Добавь deploy.resources.limits и logging с max-size/max-file всем сервисам. Урежь memory api до 32M, запусти нагрузку, поймай рестарт с кодом 137.
  • Подними cAdvisor, открой :8080 и найди по-контейнерное потребление памяти.
  • Вынеси пароль базы в secrets через файл, убери его из environment, проверь docker compose config - пароля не должно быть в выводе.
Контрольные вопросы
  • Чем depends_on с condition: service_healthy отличается от обычного depends_on и почему без healthcheck он бесполезен?
  • Почему секрет, переданный через ENV или ARG, опаснее секрета из /run/secrets, и где он может утечь?
  • Что произойдет с контейнером без memory limit при утечке памяти и кто и почему пострадает на хосте?
  • Какие шаги проходит образ в production-пайплайне от сборки до пуша и за что отвечают Trivy и cosign?
Итог урока и курса

Ты собрал не демку, а docker production стек: тонкие multi-stage образы без root, healthcheck с настоящими зависимостями, лимиты против OOM, секреты снаружи, ротацию логов, мониторинг через cAdvisor и поставку через CI со сканом и подписью. Это и есть production-ready - система, которая переживает падение зависимости и рестарт без твоего участия. Контейнеры - не магия, а аккуратная сумма cgroups, namespaces, слоев и OCI-стандарта; ты теперь видишь, что под капотом, и где обычно ломается. Дальше дорога одна - выбирай по масштабу: остаешься на Compose, уходишь в Kubernetes или Podman, добавляешь eBPF-наблюдаемость и строишь платформу. Базу ты заложил крепкую. Удачи в проде.
👍4 ❤️1 🔥1 😄 🤔1
Аватара пользователя
kernel21
Сообщения: 1
Зарегистрирован: 27 май 2026, 11:53

Re: Сквозной проект: production-ready стек и путь дальше

Сообщение kernel21 »

Дошел до финала и наконец щелкнуло, почему depends_on у меня раньше не спасал от гонки - healthcheck у базы просто не было. Поставил pg_isready и api перестал падать на старте. Спасибо за курс, реально по делу.
👍1 ❤️ 🔥 😄 🤔1
Аватара пользователя
madisoni
Сообщения: 1
Зарегистрирован: 31 май 2026, 00:43

Re: Сквозной проект: production-ready стек и путь дальше

Сообщение madisoni »

Вопрос по лимитам: cpus 1.0 это жесткий потолок в одно ядро или просто квота, и можно ли отдельно задать reservations чтобы гарантировать минимум памяти под базу?
👍 ❤️ 🔥3 😄 🤔
Ответить
← Предыдущая глава
Лучшие практики и антипаттерны Docker

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

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

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

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

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