Контейнеризация приложений: бэкенд, фронтенд, БД

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

Контейнеризация приложений: бэкенд, фронтенд, БД

Сообщение 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 по кускам - слои, кэш, сети, тома. Теперь собираем все вместе на реальной задаче: упаковать в контейнеры живое приложение. Не abstract hello-world, а то, что встречается в каждом втором проекте - бэкенд на Python, Go, Node или Java, SPA-фронтенд и база данных. У каждого из трех своя физика. Бэкенд должен правильно ловить сигналы и не запускаться из-под root. Фронт это статика, которую глупо отдавать тяжелым node-образом. БД хранит состояние, и тут одна ошибка с томом стоит тебе всех данных.

Боль, которую решаем, простая. Новички пихают в один образ исходники, компилятор, тесты, dev-зависимости и запускают процесс от рута - получается жирный дырявый образ на гигабайт, который не умеет грейсфул-шатдаун и теряет данные при пересоздании. Контейнеризация приложения - это не "обернул в Dockerfile и забыл", а набор инженерных решений под каждый тип нагрузки. Разберем их по слоям, с механикой под капотом и граблями из боевой эксплуатации.

Изображение

Бэкенд: multi-stage, непривилегированный, сигналы

Начнем с типового бэкенда. Главная идея для компилируемых и сборочных языков - multi-stage build. Ты делишь Dockerfile на стадии: в первой собираешь артефакт со всем тяжелым инструментарием, во второй копируешь только готовый бинарь или зависимости в чистый минимальный образ. В финальный образ не попадает ни компилятор, ни кэш пакетов, ни исходники.

Покажу на Go - язык дает статический бинарь, поэтому финальная стадия может быть вообще пустой.

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

# build stage
FROM golang:1.24 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

# runtime stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
Распакуем по полям. CGO_ENABLED=0 заставляет линковать статически, без зависимости от glibc - тогда бинарь живет даже в distroless static, где нет ни shell, ни libc. Образ distroless от Google не содержит пакетного менеджера и оболочки: атаковать нечего, exec в него shell-ом не зайдешь. Тег nonroot уже создает пользователя с UID 65532, поэтому USER nonroot:nonroot переключает процесс на непривилегированного юзера. Это критично: если в приложении найдут RCE, атакующий окажется не рутом, а ограниченным юзером без возможности что-то доустановить.

Для docker python и docker node multi-stage тоже работает, но смысл смещается. Интерпретируемым языкам компилятор не нужен в рантайме, зато нужны зависимости. В питоне трюк - ставить пакеты в отдельный virtualenv на build-стадии и копировать его готовым.

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

FROM python:3.13-slim AS build
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.13-slim
RUN useradd -r -u 10001 appuser
COPY --from=build /opt/venv /opt/venv
COPY --chown=appuser . /app
WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=3 \
  CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health').status==200 else 1)"
ENTRYPOINT ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
Теперь про две вещи, которые отличают грамотный бэкенд-контейнер от любительского: сигналы и healthcheck.

Сигналы. Когда оркестратор останавливает контейнер, он шлет PID 1 сигнал SIGTERM, ждет grace period (по умолчанию 10 секунд) и добивает SIGKILL. Если твой процесс не PID 1 или не умеет ловить SIGTERM, ты получаешь жесткое убийство: оборванные запросы, незакрытые транзакции, поврежденные файлы. Есть две ловушки. Первая - shell-форма ENTRYPOINT. Если написать ENTRYPOINT node server.js (без скобок), Docker запустит это как /bin/sh -c "node server.js", и PID 1 станет shell, который сигналы наследникам не пробрасывает. Всегда используй exec-форму с массивом: ENTRYPOINT ["node", "server.js"]. Вторая ловушка - зомби-процессы: если приложение само порождает дочерние процессы, ему нужен init, который их пожинает. Для этого есть встроенный флаг docker run --init (он подкладывает tini в PID 1) или ENTRYPOINT через tini вручную. В коде же надо явно повесить хендлер на SIGTERM и сделать graceful shutdown - дать серверу дослать ответы и закрыть пул соединений.

Healthcheck. Инструкция HEALTHCHECK периодически дергает команду внутри контейнера. Поля разбираются так: interval - как часто проверять, timeout - сколько ждать ответа, start-period - окно прогрева, в течение которого падения не считаются (важно для Java с долгим стартом JVM), retries - сколько подряд провалов до статуса unhealthy. Посмотреть статус:

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

$ docker ps
CONTAINER ID   IMAGE      STATUS                   PORTS
a1b2c3d4e5f6   api:1.0    Up 2 minutes (healthy)   0.0.0.0:8000->8000/tcp
Поле STATUS показывает (healthy), (unhealthy) или (health: starting). Подробности и историю последних проверок выдаст docker inspect --format '{{json .State.Health}}' api - там увидишь ExitCode и stdout каждой пробы, что бесценно при отладке. Важно: сам по себе healthcheck в чистом docker контейнер не перезапускает, он только меняет статус. Перезапуском по unhealthy занимается оркестратор (Swarm, k8s через свои probes) или твоя политика. Не делай проверку тяжелой - не дергай в ней базу полным запросом, иначе healthcheck сам станет источником нагрузки.

Про docker java отдельная заметка: тащи в рантайм JRE, а не полный JDK, бери eclipse-temurin:21-jre вместо jdk, и для свежих JVM ставь флаги памяти осознанно - современная JVM уважает cgroup-лимиты контейнера, но проверь это под своей версией, чтобы heap не вылез за память пода и не словить OOMKill.

Фронтенд SPA: собрать в node, отдать через docker nginx

Со SPA (React, Vue, Angular) частая ошибка - оставить в продакшене dev-сервер вроде vite или webpack-dev-server. Это медленно, небезопасно и жрет память. На самом деле собранный фронт - это просто папка статики: html, js, css. Для статики не нужен Node вообще, ее идеально отдает легкий веб-сервер. Поэтому каноничный паттерн - multi-stage, где docker node собирает бандл, а docker nginx его раздает.

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

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
Что мы выиграли. Образ node:22 весит сотни мегабайт, nginx:alpine - около 50. В финальный образ не попадает node_modules с тысячами файлов и потенциальных уязвимостей - там только готовая статика плюс nginx. npm ci вместо npm install берется не случайно: ci ставит строго по package-lock.json, не трогает его и валится при расхождении - это воспроизводимая сборка, а не "что подвезли сегодня".

Конфиг nginx для SPA обязан уметь одну вещь - отдавать index.html на любой неизвестный путь, иначе клиентский роутинг сломается при перезагрузке страницы на /profile или /orders/42.

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

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}
Директива try_files $uri $uri/ /index.html - сердце SPA-конфига: nginx сначала ищет реальный файл, потом каталог, и если ничего нет - отдает index.html, а дальше роутер на фронте сам разбирается. Хэшированные ассеты кэшируем агрессивно на год с immutable, потому что при пересборке у них меняется имя. Маленький нюанс безопасности: официальный nginx стартует мастер-процессом от root (нужен для bind на 80) и форкает воркеры от nginx-юзера. Если хочешь полностью rootless фронт - бери образ nginx-unprivileged, он слушает 8080 и работает без рутового мастера.

Docker база данных: можно в dev, осторожно в проде

Самая нагруженная тема. БД в контейнере - это абсолютно нормально для разработки и staging: одна команда поднимает чистый postgres нужной версии, и у всей команды одинаковое окружение. А вот в продакшене docker база данных требует трезвой головы.

Ключевая разница между БД и stateless-бэкендом - состояние. Контейнер по природе эфемерен: пересоздал - все, что было внутри, исчезло. Для базы это смертельно. Поэтому железное правило: данные БД живут в томе, не в слое образа. Том - это директория на хосте (или managed volume), которую Docker монтирует внутрь контейнера; она переживает пересоздание и обновление контейнера.

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

$ docker volume create pgdata
$ docker run -d --name pg \
    -e POSTGRES_PASSWORD=secret \
    -v pgdata:/var/lib/postgresql/data \
    -p 5432:5432 \
    postgres:17
Путь /var/lib/postgresql/data - это PGDATA, где postgres держит файлы базы. Монтируя туда именованный том pgdata, мы выносим состояние наружу контейнера. Снес контейнер, поднял новый из того же образа с тем же томом - данные на месте. Проверить:

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

$ docker volume inspect pgdata
[
    {
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Name": "pgdata",
        "Driver": "local"
    }
]
Поле Mountpoint показывает реальное место на хосте под управлением Docker (/var/lib/docker/volumes/...). Большинство официальных образов БД при первом старте на пустом томе выполняют init-скрипты из /docker-entrypoint-initdb.d/ - туда монтируешь .sql или .sh для создания схемы и сидов. Важнейший нюанс: эти скрипты отрабатывают только если каталог данных пуст. Том уже инициализирован - init молча пропускается. Поэтому не надейся на initdb-скрипты для миграций существующей базы, это исключительно первичный бутстрап.

Почему в проде осторожно. Во-первых, IO и производительность: слой overlay2 и неправильно выбранный драйвер тома добавляют накладные расходы, а БД чувствительна к латентности диска - под серьезной нагрузкой нужен том на быстром локальном NVMe, а не на сетевом хранилище с непредсказуемой задержкой. Во-вторых, бэкапы и восстановление: контейнер не отменяет необходимость pg_dump/pg_basebackup, репликации и проверенного плана восстановления - оркестратор может пересоздать под на другом узле, и если том не следует за ним, ты потерял базу. В-третьих, апгрейды мажорных версий postgres требуют pg_upgrade, а не просто смены тега образа - новый бинарь не прочитает старый формат каталога. Поэтому многие команды осознанно держат прод-БД как managed-сервис (RDS, Yandex Managed PostgreSQL, облако VK) или на bare-metal, а в контейнерах гоняют только dev и staging. Это не догма, БД в Kubernetes через зрелые операторы (CloudNativePG, Zalando) работает в проде - но это отдельная инженерная дисциплина с резервированием и failover, а не "просто docker run postgres".

Конфиг через env и сборка стека в Compose

Контейнер должен быть неизменяемым артефактом, а отличия между dev, staging и prod - снаружи, через переменные окружения. Это принцип config из методологии twelve-factor. Один и тот же образ бэкенда читает DATABASE_URL и LOG_LEVEL из env и ведет себя по-разному в разных средах, не пересобираясь.

Собираем все три компонента в стек. Compose v2 (команда docker compose, файл compose.yaml без устаревшего ключа version) описывает сервисы, сети и тома декларативно.

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

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build: ./backend
    environment:
      DATABASE_URL: postgres://app@db:5432/app
      LOG_LEVEL: info
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8000:8000"

  web:
    build: ./frontend
    depends_on:
      - api
    ports:
      - "8080:80"

volumes:
  pgdata:

secrets:
  db_password:
    file: ./secrets/db_password.txt
Разберем неочевидное. Сервис api обращается к базе по имени db, а не по IP - Compose поднимает общую сеть с встроенным DNS, и имя сервиса резолвится в его адрес. depends_on с condition: service_healthy - это не просто "стартуй после", а "ждать, пока healthcheck базы не станет healthy". Без condition depends_on гарантирует только порядок запуска контейнера, но не готовность процесса внутри - база может еще инициализироваться, а api уже ломится с коннектом и падает. Поэтому healthcheck у db с pg_isready здесь не для красоты.

Теперь самый частый антипаттерн - секреты. В примере пароль идет не как POSTGRES_PASSWORD в открытом env, а через POSTGRES_PASSWORD_FILE и механизм secrets, который монтирует файл в /run/secrets/db_password (это tmpfs в памяти, не на диске и не в слое образа). Почему это важно: переменные окружения видны всем процессам контейнера, утекают в docker inspect, в логи, в дочерние процессы и в crash-репорты. Пароль в env - это пароль на виду. Файловые секреты живут в памяти и не светятся в инспекте.

Антипаттерны и грабли из практики
  • Данные БД внутри образа. Никогда не клади дамп или каталог данных в слой образа через COPY. Образ неизменяем и пересобирается, том - нет. Данные в образе = данные, которые ты гарантированно потеряешь и которые раздуют образ до неприличия.
  • Секреты в ENV и в Dockerfile. ENV DB_PASSWORD=... в Dockerfile вмораживает секрет в слой навсегда - его видно в docker history любому, у кого есть образ. Для build-time секретов (токен приватного репозитория) используй RUN --mount=type=secret BuildKit, для рантайма - docker secrets или внешний vault.
  • root по умолчанию. Без инструкции USER процесс бежит от рута. Контейнерный root - это не полная изоляция от хостового, и при цепочке уязвимостей это лишний шаг к побегу. Всегда явный непривилегированный USER.
  • latest как тег. FROM postgres:latest делает сборку невоспроизводимой - сегодня и завтра это разные образы. Пинуй мажорную версию, а в идеале digest по sha256.
  • Тяжелый dev-образ в проде. node-dev-сервер для SPA, JDK вместо JRE, отсутствие multi-stage - все это лишние сотни мегабайт и десятки CVE в финальном образе.
  • Игнор .dockerignore. Без него в контекст сборки и в COPY . попадают .git, node_modules, локальные .env с секретами. Это и медленно, и опасно.
Перед выкаткой прогоняй образ сканером цепочки поставок - docker scout cves или Trivy покажут известные уязвимости в слоях, а для целостности артефактов в зрелом пайплайне образы подписывают через cosign и прикладывают SBOM. Это уже не паранойя, а норма supply chain security в 2026.

Мини-лаба: подними стек руками
  • Создай папки backend и frontend. В backend положи простейший HTTP-сервер на питоне с эндпоинтами / и /health, в frontend - любой собранный SPA (хоть статичный index.html в dist).
  • Напиши для бэкенда multi-stage Dockerfile с непривилегированным USER, exec-формой ENTRYPOINT и HEALTHCHECK на /health.
  • Для фронта сделай Dockerfile node-build -> nginx с try_files на index.html.
  • Создай файл secrets/db_password.txt и собери compose.yaml из трех сервисов с томом pgdata и depends_on по service_healthy.
  • Запусти docker compose up --build, дождись (healthy) у db в docker compose ps, открой фронт на 8080 и проверь, что api ходит в базу.
  • Сделай docker compose down (без -v!), снова up - убедись, что данные в pgdata выжили. Потом docker compose down -v и увидь, что том и данные исчезли.
Контрольные вопросы
  • Почему shell-форма ENTRYPOINT ломает грейсфул-шатдаун и как это связано с PID 1 и SIGTERM?
  • Зачем для SPA нужен docker nginx с try_files, и что произойдет без этой директивы при перезагрузке страницы на вложенном маршруте?
  • Чем именованный том отличается от слоя образа для docker база данных, и почему init-скрипты иногда молча не выполняются?
  • Почему секрет в переменной окружения - плохо, и какие два механизма (build-time и runtime) дают безопасную альтернативу?
Итог

Контейнеризация приложения - это три разных инженерных задачи под одной крышей. Бэкенд: multi-stage, непривилегированный юзер, exec-ENTRYPOINT с обработкой SIGTERM и осмысленный healthcheck. Фронтенд: собрать в docker node, отдать легким docker nginx с try_files. БД: данные строго в томе, init только на пустом каталоге, в проде - трезвая оценка IO, бэкапов и апгрейдов. Конфиг наружу через env, секреты - через файлы и docker secrets, а не открытым текстом. Связываем все в Compose v2 с depends_on по healthcheck. Сделаешь так - получишь стек, который не разваливается при первом же рестарте и не отдает пароль каждому, кто запустил docker inspect.
👍4 ❤️ 🔥 😄 🤔1
Аватара пользователя
react_addict
Сообщения: 1
Зарегистрирован: 06 июн 2026, 09:38

Re: Контейнеризация приложений: бэкенд, фронтенд, БД

Сообщение react_addict »

Затупил на depends_on - думал он сам ждет готовности базы, а оказалось без condition service_healthy api успевает стартануть раньше и падает на коннекте. Добавил healthcheck с pg_isready, заработало. Спасибо за разбор
👍3 ❤️1 🔥 😄 🤔
Аватара пользователя
sleepystack
Сообщения: 1
Зарегистрирован: 15 май 2026, 21:11

Re: Контейнеризация приложений: бэкенд, фронтенд, БД

Сообщение sleepystack »

Вопрос по проду: если БД в контейнере такая боль с бэкапами и апгрейдами, реально ли вообще кто-то держит постгрес в k8s на проде или все уходят в managed? Или это только с операторами вроде CloudNativePG имеет смысл
👍1 ❤️2 🔥 😄 🤔1
Ответить
← Предыдущая глава
Docker Desktop, WSL2 и альтернативы на Windows и Mac
Следующая глава →
Docker и Kubernetes: containerd, миграция, kompose

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

Поделиться темой: ✈ Telegram VK
Похожие запросы: docker network как связать контейнеры между собой по имени

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

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

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