Все уроки
DevOps и облакоСредний13 мин чтенияВ моём стеке

Docker

Упакуйте приложение со всем, что ему нужно, и запускайте его одинаково где угодно.

  • Images
  • Containers
  • Compose
  • Volumes
  • Multi-stage builds
Шаг 1/4
«А у меня на машине работает!» — но на сервере другая версия PHP, и всё ломается.

За 30 секунд

Docker упаковывает ваше приложение, его рантайм и библиотеки в образ (image) — запечатанную коробку с версией. Запуск этого образа даёт контейнер: изолированный процесс, который ведёт себя одинаково на вашем ноутбуке, в CI и на сервере. А Docker Compose описывает всю систему (приложение, базу, кэш, воркеры, веб-сервер) в одном файле, который запускается одной командой.

Почему я это использую

  • Я использую его, потому что «а у меня на машине работает» перестаёт быть отговоркой: на сервере крутится ровно тот образ, который CI собрал и протестировал.
  • Я использую Compose, чтобы описать всю систему в одном читаемом файле: новый разработчик за несколько минут поднимает приложение, базу, Redis и воркеры.
  • Я использую его, потому что обновления становятся скучными: поменял тег образа, пересобрал, а если что-то сломалось — откатился, указав предыдущий тег.
  • Он держит серверы чистыми. Разным приложениям могут быть нужны разные версии PHP или Node, и они не дерутся за то, что установлено на хосте.

Как это работает

01Образы, контейнеры и чем они отличаются от VM

Образ — это шаблон только для чтения: снимок файловой системы плюс метаданные, например какую команду запускать. Контейнер — запущенный экземпляр образа с тонким записываемым слоем сверху. Представьте образ как карточку с рецептом, а контейнер — как блюдо, приготовленное по ней; по одной карточке можно приготовить много блюд. Виртуальная машина эмулирует целый компьютер со своим ядром, поэтому она загружает полноценную операционную систему и обычно занимает гигабайты и десятки секунд на старт. Контейнер же — обычный Linux-процесс, который ядро изолирует с помощью namespaces (что он видит: файлы, сеть, список процессов) и ограничивает через cgroups (сколько CPU и памяти он может использовать). Он делит ядро с хостом, поэтому стартует за миллисекунды и почти не добавляет накладных расходов. Обратная сторона: изоляция слабее, чем у VM, поэтому облака часто запускают контейнеры внутри виртуальных машин.

  • Образ = неизменяемый шаблон, определяется именем name:tag и digest'ом содержимого.
  • Контейнер = образ + записываемый слой + изолированный процесс.
  • Контейнеры делят ядро хоста; у VM ядро своё.

02Слои и кэширование сборки

Каждая инструкция в Dockerfile (FROM, COPY, RUN) создаёт слой, а образ — это стопка слоёв, объединённых overlay-файловой системой. Слои кэшируются по содержимому: если инструкция и всё, что до неё, не изменились, Docker переиспользует слой из кэша, а не выполняет его заново. Как только меняется один слой, все последующие пересобираются. Это правило и определяет скорость ваших сборок. Сначала скопируйте манифест зависимостей (package.json и lockfile или composer.json и composer.lock), установите зависимости и только потом копируйте остальной код. Теперь правка компонента не запускает полный npm install. Файл .dockerignore не пускает в контекст сборки node_modules, .git, логи и .env-файлы — сборки становятся быстрее, а секреты не утекают в образы.

bash
# .dockerignore
node_modules
.next
.git
*.log
.env*
Dockerfile
compose*.yaml

03Продакшен-Dockerfile: multi-stage, компактный, не от root

Multi-stage сборка использует один этап с компиляторами и dev-зависимостями, чтобы собрать приложение, а затем копирует только результат в чистый и маленький runtime-этап. В финальном образе нет случайных source map'ов, нет инструментов сборки и гораздо меньше пакетов, которые нужно патчить. Используйте slim- или Alpine-образы и фиксируйте мажорную версию вместо latest. Запускайте от непривилегированного пользователя: если кто-то взломает приложение, он окажется обычным пользователем, а не root. Пример ниже собирает Next.js-приложение с output: "standalone" в next.config — это даёт минимальный server.js и только те node_modules, которые реально нужны. Тот же подход работает и для Laravel: этап Composer, этап Node для ассетов и финальный этап PHP-FPM.

dockerfile
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
 
FROM node:24-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
 
FROM node:24-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public
USER node
CMD ["node", "server.js"]

04Volumes и сети

Записываемый слой контейнера исчезает вместе с контейнером, поэтому всё, что нужно сохранить (файлы базы данных, загрузки), должно жить в volume. Именованные volume'ы управляются Docker'ом — это правильный выбор для баз данных. Bind mount пробрасывает папку хоста внутрь контейнера и идеален для разработки, когда изменения в коде должны появляться мгновенно. Что касается сети: контейнеры в одной пользовательской сети находят друг друга по имени сервиса через встроенный DNS Docker'а — приложение подключается к db:5432, а не к localhost. Внутри контейнера localhost означает сам этот контейнер — это самая частая путаница у новичков. Публикуйте только те порты, которые действительно нужны внешнему миру, обычно только 80 и 443 на reverse proxy.

  • Именованный volume: постоянные данные под управлением Docker (базы данных).
  • Bind mount: папка хоста, отлично подходит для живого кода в разработке.
  • Опубликованные порты во многих конфигурациях обходят правила ufw; привязывайте приватные порты к 127.0.0.1 или не публикуйте их вовсе.

05Docker Compose для целого приложения

Compose описывает несколько контейнеров, их сети и volume'ы в одном compose.yaml, а docker compose up -d запускает всё сразу. Ниже — типичное веб-приложение: nginx впереди как единственная публичная точка входа, контейнер приложения, воркер очереди из того же образа, но с другой командой, PostgreSQL с постоянным volume и health check'ом и Redis для кэша и очередей. depends_on с condition: service_healthy заставляет приложение ждать, пока база действительно начнёт принимать соединения, а не просто пока её контейнер стартует. Обратите внимание: база и Redis вообще не публикуют порты, снаружи доступен только nginx. Секреты берутся из env-файла, который не коммитится в Git.

yaml
services:
  nginx:
    image: nginx:stable-alpine
    ports: ["80:80", "443:443"]
    depends_on: [app]
  app:
    build: .
    env_file: .env
    depends_on:
      db: { condition: service_healthy }
  worker:
    build: .
    command: php artisan queue:work --tries=3
  db:
    image: postgres:17
    volumes: [db-data:/var/lib/postgresql/data]
    healthcheck: { test: ["CMD", "pg_isready", "-U", "postgres"], interval: 5s }
  redis:
    image: redis:7-alpine
volumes: { db-data: {} }

06Compose для разработки и для продакшена

Разработке и продакшену нужно разное. В разработке хочется код через bind mount, hot reload, инструменты отладки и, возможно, перехватчик почты. В продакшене — собранные образы с «запечённым» кодом, restart: unless-stopped, лимиты ресурсов, ротацию логов, TLS и мониторинг. Не поддерживайте два расходящихся файла. Держите базовый compose.yaml с общей частью и накладывайте сверху override-файлы: compose.override.yaml подхватывается автоматически в разработке, а продакшен запускается явно — базовый файл плюс продакшен-файл. Можно добавить и другие слои, например файл для TLS или для observability, который добавляет Prometheus, Grafana и Loki. Перед деплоем проверяйте итоговую склейку через docker compose config.

bash
# development: compose.yaml + compose.override.yaml (auto-loaded)
docker compose up -d
 
# production: explicit files, no override
docker compose -f compose.yaml -f compose.prod.yaml config --quiet
docker compose -f compose.yaml -f compose.prod.yaml up -d --build
 
# useful day to day
docker compose ps
docker compose logs -f --tail=100 app
docker compose exec app sh

Когда применять

  • Любое приложение из нескольких частей (приложение, база, кэш, воркеры), которое новые разработчики должны быстро запускать.
  • Когда на серверах нужно держать рядом разные версии рантайма без конфликтов.
  • Когда вы хотите, чтобы CI собирал один артефакт, который без изменений проходит путь от тестирования до продакшена.
  • Как первый шаг перед любым оркестратором: Kubernetes, ECS и Cloud Run запускают контейнерные образы.

Когда не стоит

  • Один статический сайт или небольшой PHP-сайт на shared-хостинге — Docker добавит движущихся частей без всякой пользы.
  • Когда в команде некому отвечать за обновления образов и патчи безопасности: устаревшие образы тихо гниют.
  • Десктопные GUI-приложения или задачи, которым нужно другое ядро, — используйте VM.

Альтернативы

  • Обычная настройка VPS

    nginx, PHP-FPM или Node с systemd/pm2, установленные напрямую. Проще для одного-двух приложений, но труднее воспроизвести.

  • Podman

    Контейнерный движок без демона и по умолчанию rootless, в основном совместим с командами Docker и файлами Compose.

  • Виртуальные машины

    Более сильная изоляция и полноценная ОС на каждую задачу; тяжелее и медленнее стартуют.

  • Platform as a service

    Платформы в стиле Heroku сами собирают и запускают ваше приложение; меньше контроля, меньше эксплуатационной работы.

Лучшие практики

  1. Используйте multi-stage сборки и slim-образы; фиксируйте мажорные версии вместо latest.
  2. Располагайте шаги Dockerfile от редко меняющихся к часто меняющимся, чтобы кэш работал на вас.
  3. Запускайте контейнеры не от root и никогда не «запекайте» секреты в образы — передавайте их в рантайме.
  4. Открывайте наружу только reverse proxy; базы данных и Redis держите во внутренней сети.
  5. Добавьте health check'и и политики перезапуска, чтобы сбои обнаруживались и устранялись автоматически.
  6. Настройте ротацию логов (драйвер json-file по умолчанию растёт бесконечно) и регулярно удаляйте старые образы.
  7. Пересобирайте образы по расписанию, чтобы получать патчи безопасности базовых образов.

Частые ошибки

  • Хранить данные базы в записываемом слое контейнера и потерять их при docker compose down -v или пересборке.
  • Опубликовать порт базы на 0.0.0.0 и думать, что firewall хоста по-прежнему его блокирует.
  • Копировать весь проект до установки зависимостей, так что каждое изменение кода переустанавливает всё.
  • Использовать localhost внутри контейнера, чтобы достучаться до другого контейнера, вместо имени сервиса.
  • Отправлять в образе .env-файлы или папку .git, потому что не было .dockerignore.

Глубже

07Что такое контейнер на самом деле: цепочка runtime и PID 1

Когда вы пишете docker run, CLI обращается к Docker daemon (dockerd), тот передаёт задачу containerd, а containerd через низкоуровневый OCI runtime, обычно runc, создаёт процесс. runc запрашивает у ядра новые namespace, привязывает процесс к cgroup, монтирует слои образа через overlayfs и уходит с дороги. Форматы образов и runtime — открытые стандарты OCI, поэтому Podman, Kubernetes и Docker могут запускать один и тот же образ. Одна деталь регулярно кусает в продакшене: первый процесс в контейнере — это PID 1. Ядро не даёт PID 1 обработчиков сигналов по умолчанию, поэтому процесс, который сам не установил обработчик SIGTERM, просто его игнорирует, а ещё от PID 1 ожидается, что он будет «прибирать» zombie-процессы. Крошечный init вроде tini, который в Compose включается через init: true, пробрасывает сигналы и собирает зомби за вас.

yaml
services:
  app:
    image: registry.example.com/app:1.8.0
    init: true               # tini as PID 1: forwards signals, reaps zombies
    stop_grace_period: 30s   # time between SIGTERM and SIGKILL (default 10s)
    stop_signal: SIGTERM

08Харденинг контейнера в рантайме

Пользователь без root — это первый шаг, а не последний. По умолчанию контейнер всё равно получает набор Linux capabilities, доступную для записи корневую файловую систему и возможность повысить привилегии через setuid-бинарники. Уберите все capabilities и верните только нужные (веб-серверу, который слушает порт 80, нужен NET_BIND_SERVICE; большинству Node- и PHP-приложений за прокси не нужно ничего). Сделайте корневую файловую систему read-only и дайте приложению небольшой tmpfs для временных файлов, чтобы атакующий не мог закинуть свои инструменты на диск. Включите no-new-privileges, чтобы трюки с setuid не срабатывали. Стандартный seccomp-профиль Docker уже блокирует десятки опасных системных вызовов — никогда не отключайте его через seccomp=unconfined. И главное: никогда не монтируйте /var/run/docker.sock в контейнер приложения — доступ к сокету равносилен root на хосте. Для дополнительного слоя защиты rootless-режим запускает сам daemon от обычного пользователя.

yaml
services:
  app:
    user: "1000:1000"
    read_only: true
    tmpfs: [/tmp]
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    deploy:
      resources:
        limits: { cpus: "1.0", memory: 512M }
    pids_limit: 200

09Docker, iptables и сюрприз с файрволом

Когда вы публикуете порт через ports: "5432:5432", Docker прописывает в iptables NAT-правила, чтобы трафик на порт хоста перенаправлялся в контейнер. Эти правила обрабатываются в цепочках PREROUTING и FORWARD — раньше, чем цепочка INPUT, где ufw хранит свои правила. В итоге ufw status говорит, что порт 5432 закрыт, а база при этом отвечает из интернета. Есть три безопасных решения. Вообще не публикуйте внутренние сервисы: контейнерам в одной сети это не нужно. Если какой-то утилите на хосте нужно достучаться до контейнера, привяжите порт к loopback: 127.0.0.1:5432:5432. Если действительно нужно фильтровать опубликованные порты, пишите правила в цепочку DOCKER-USER — Docker проверяет её до своих правил и никогда не перезаписывает. После любого изменения проверяйте снаружи сканированием портов, а не доверяйте отчёту самого файрвола.

yaml
services:
  db:
    image: postgres:17
    # reachable only from the host itself, never from the internet
    ports: ["127.0.0.1:5432:5432"]
  redis:
    image: redis:7-alpine
    # no ports at all: only containers on the same network can connect
 
# from another machine: nmap -Pn -p 5432,6379 203.0.113.10

10Более быстрые и безопасные сборки с BuildKit

BuildKit, сборщик по умолчанию в актуальных версиях Docker, умеет то, чего не даёт обычное кэширование слоёв. Cache mount сохраняет кэш загрузок пакетного менеджера между сборками, не помещая его ни в один слой, поэтому после изменения lockfile пакеты ставятся из тёплого кэша, а не из сети. Secret mount открывает токен (для приватного npm registry или Composer auth) только одному шагу RUN; он никогда не попадает в историю образа, в отличие от значений ARG или ENV, которые любой может прочитать через docker history. В CI раннеры стартуют пустыми, поэтому экспортируйте кэш в registry и импортируйте его при следующем запуске. Для воспроизводимости закрепляйте базовые образы не только по тегу, но и по digest, и пусть автоматика вроде Renovate или Dependabot открывает pull request, когда digest меняется. BuildKit также умеет прикладывать SBOM и provenance attestation, которые сканеры безопасности и аудиторы ожидают всё чаще.

dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci
 
# build: docker build --secret id=npmrc,src=$HOME/.npmrc .
# CI:    docker buildx build --cache-from type=registry,ref=registry.example.com/app:cache \
#          --cache-to type=registry,ref=registry.example.com/app:cache,mode=max --push .

Реальные ситуации

01

Диск сервера заполнен, и виноват Docker

Проблема

Мониторинг предупреждает, что корневой диск заполнен на 95%. Приложение пока работает, но база перестанет принимать записи, когда место кончится, а сервер никто не чистил уже несколько месяцев.

Как решить

Начните с docker system df, чтобы понять, что занимает место: образы, build cache, volume или контейнеры. Старые образы удалите через docker image prune -a --filter until=168h, а build cache — через docker builder prune; никогда не чистите volume вслепую, потому что именно там живут данные базы. Затем проверьте логи контейнеров в /var/lib/docker/containers: драйвер json-file по умолчанию никогда их не ротирует. Задайте max-size и max-file в /etc/docker/daemon.json, перезапустите daemon и пересоздайте контейнеры, потому что настройки логов применяются только к новым контейнерам. В конце добавьте еженедельную задачу prune и алерт на заполнение диска при 80%, чтобы это больше никогда не становилось срочным.

02

Контейнер постоянно перезапускается с exit code 137

Проблема

После релиза контейнер приложения перезапускается каждые несколько минут. Логи обрываются без stack trace, а docker ps показывает растущий счётчик перезапусков.

Как решить

Exit code 137 означает, что процесс получил SIGKILL, и обычно его отправляет OOM killer. Подтвердите это через docker inspect --format '{{.State.OOMKilled}}' app и следите за потреблением через docker stats. Затем решите, что не так — лимит или само приложение. Для Node задайте --max-old-space-size ниже лимита контейнера, иначе heap может вырасти за него раньше, чем сработает garbage collection. Ищите утечку, сравнивая память во времени: часто это неограниченный in-memory кэш или растущий массив слушателей. Повышайте лимит, только если реальное потребление это оправдывает, и добавьте алерт по памяти, чтобы следующая регрессия проявилась раньше, чем перезапуски.

03

База доступна из интернета, несмотря на ufw

Проблема

Плановое сканирование показывает, что PostgreSQL отвечает на публичном IP. ufw явно запрещает порт 5432, и команда не понимает, как проходит трафик.

Как решить

Сначала отнеситесь к этому как к инциденту. Смените пароль базы и проверьте её логи на незнакомые входы. Затем устраните причину: Compose-файл публикует 5432:5432, и правила iptables от Docker пробрасывают этот трафик раньше, чем его увидит ufw. Уберите запись ports, чтобы подключаться могли только контейнеры из внутренней сети, или привяжите порт к 127.0.0.1, если он нужен утилите на хосте. Пересоздайте сервис и проверьте с другой машины через nmap -Pn -p 5432 203.0.113.10. Добавьте такое же внешнее сканирование в ежемесячный чек-лист и проверьте все Compose-файлы на опубликованные порты, кроме 80 и 443.

Шпаргалка

Посмотреть, что занимает диск
docker system df -v
Удалить образы старше недели
docker image prune -a --filter until=168h
Ротация логов контейнеров (daemon.json)
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
Проверить результат health check
docker inspect --format '{{json .State.Health}}' app
Убит из-за памяти?
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app
CPU и память по контейнерам в реальном времени
docker stats --no-stream
Пересобрать и перезапустить только один сервис
docker compose up -d --no-deps --build app
Проверить объединённые Compose-файлы
docker compose -f compose.yaml -f compose.prod.yaml config --quiet
Просканировать образ на известные CVE
trivy image registry.example.com/app:1.8.0
Показать, кто собрал какой слой
docker history --no-trunc registry.example.com/app:1.8.0

В моих реальных проектах

  • Multi-tenant POS SaaS работает на выделенном сервере с Docker Compose: отдельные override-файлы для dev, prod, TLS и observability.
  • В этом стеке nginx стоит reverse proxy перед приложением, а PostgreSQL, Redis и воркеры BullMQ — отдельные сервисы, и всё это за CDN.
  • Laravel-проекты используют Sail (Docker) для локальной разработки, так что у каждого разработчика одинаковые версии PHP, базы и Redis.

Вопросы и ответы

Q1Чем образ отличается от контейнера?

Образ — неизменяемый многослойный шаблон: файловая система плюс метаданные вроде команды по умолчанию и открытых портов. Он определяется тегом и, точнее, digest'ом содержимого. Контейнер — запущенный экземпляр этого образа: Docker добавляет сверху тонкий записываемый слой и запускает изолированный процесс со своими namespaces и лимитами cgroup. Из одного образа можно запустить много контейнеров, а удаление контейнера выбрасывает его записываемый слой — поэтому постоянные данные должны жить в volume'ах. Хорошая ментальная модель: образ — это класс, контейнер — объект.

Q2Чем Docker отличается от виртуальной машины?

Виртуальная машина запускает полноценную гостевую операционную систему со своим ядром поверх гипервизора, поэтому она тяжёлая: гигабайты на диске, собственный процесс загрузки и сильная изоляция на уровне железа. Контейнер — обычный процесс на хосте, который ядро Linux изолирует через namespaces и ограничивает через cgroups. Он делит ядро с хостом, поэтому стартует за миллисекунды и почти не имеет накладных расходов. Цена — более слабая изоляция: уязвимость в ядре затрагивает все контейнеры на хосте. На практике их комбинируют: облачные провайдеры запускают ваши контейнеры внутри VM, получая плотность от контейнеров и изоляцию от границы VM.

Q3Как сделать Docker-образы меньше, а сборки быстрее?

Во-первых, multi-stage сборки: компилируете на этапе со всеми инструментами, а в slim runtime-образ копируете только результат сборки. Во-вторых, выбирайте маленькую базу вроде Alpine или slim-варианта Debian и ставьте только то, что нужно приложению в рантайме. В-третьих, расставьте инструкции с расчётом на кэш: скопировать lockfile, установить зависимости, потом скопировать код — тогда правки кода не переустанавливают пакеты. В-четвёртых, строгий .dockerignore не пускает в контекст node_modules, .git и результаты сборки. И наконец, cache mount'ы BuildKit и кэш в registry в CI позволяют повторным сборкам переиспользовать загрузки между запусками.

Q4Чем CMD отличается от ENTRYPOINT?

ENTRYPOINT задаёт исполняемый файл, который контейнер запускает всегда; CMD задаёт аргументы по умолчанию или команду по умолчанию, если ENTRYPOINT нет. Аргументы, переданные в docker run, заменяют CMD, но добавляются к ENTRYPOINT. Распространённый паттерн — entrypoint-скрипт, который готовит окружение (например, ждёт базу или кэширует конфиг), а затем выполняет exec "$@", передавая управление CMD. Используйте JSON-форму массива, например CMD ["node", "server.js"], чтобы ваш процесс работал как PID 1 и напрямую получал сигналы остановки. Shell-форма оборачивает его в sh, который может проглотить SIGTERM и сломать graceful shutdown.

Q5Почему контейнер приложения не может подключиться к базе по localhost?

Потому что внутри контейнера localhost — это собственный сетевой namespace этого контейнера, а не хост и не другие контейнеры. В Compose все сервисы подключаются к общей сети, а Docker предоставляет DNS, поэтому приложение должно подключаться по имени сервиса, например хост db и порт 5432. Убедитесь, что оба сервиса в одной сети, что база действительно готова (используйте health check с depends_on condition: service_healthy) и что внутри своего контейнера она слушает все интерфейсы. Публиковать порт базы для этого не нужно: публикация нужна только для трафика извне Docker.

Q6Достаточно ли Docker Compose для продакшена?

Для одного сервера — да, и часто это правильный выбор. Compose даёт декларативные сервисы, health check'и, политики перезапуска, volume'ы и сети, и небольшой команде его легко понять. Чего он не даёт — так это планирования на несколько хостов, автоматического failover между машинами и встроенных rolling update'ов. Поэтому он хорошо подходит, когда один правильно подобранный сервер с хорошими бэкапами и мониторингом укладывается в вашу цель по доступности. Когда нужно несколько нод, деплой без простоя по всем ним и автоматический перезапуск после падения ноды — смотрите на Kubernetes, ECS или инструменты полегче вроде Kamal или Swarm.

Вопросы уровня senior

S1Как работает graceful shutdown для контейнеров и почему некоторые приложения теряют запросы при каждом деплое?

При docker stop или пересоздании в Compose Docker отправляет PID 1 стоп-сигнал (по умолчанию SIGTERM), ждёт grace period (по умолчанию 10 секунд) и затем шлёт SIGKILL. Запросы теряются, когда сигнал не доходит до приложения или приложение его игнорирует. Shell-форма CMD запускает приложение под sh, который не пробрасывает сигналы; процесс, работающий как PID 1 без собственного обработчика, полностью игнорирует SIGTERM. Решение — exec-форма CMD, init вроде tini и приложение, которое по SIGTERM перестаёт принимать соединения, дожидается завершения текущих запросов и закрывает пулы соединений с базой. Grace period должен быть дольше самого медленного запроса или задачи. За прокси также убедитесь, что прокси перестаёт направлять трафик на старый контейнер до его остановки или повторяет идемпотентные запросы.

S2Alpine, Debian slim или distroless: как выбрать базовый образ?

Alpine крошечный, в нём мало пакетов, которые надо патчить, но вместо glibc он использует musl. Большей части кода это безразлично, однако нативные модули, некоторые Python wheels, пограничные случаи DNS-резолвинга и чувствительный к производительности код могут вести себя иначе или требовать сборки из исходников, что замедляет билды. Образы Debian или Ubuntu slim больше, но используют glibc, поэтому готовые бинарники просто работают, а инструменты для отладки легко добавить. Distroless-образы содержат только runtime и ваше приложение, без shell и пакетного менеджера: это уменьшает поверхность атаки и список CVE, но усложняет интерактивную отладку — приходится полагаться на логи, метрики и эфемерные debug-контейнеры. Мой выбор по умолчанию — slim ради совместимости, Alpine — когда я проверил на нём весь стек, и distroless — для зрелых сервисов с хорошей observability.

S3Как защитить supply chain контейнеров — от базового образа до продакшена?

Думайте о трёх этапах. На этапе сборки: минимальные закреплённые базовые образы, multi-stage сборки, чтобы компиляторы никогда не попадали в продакшен, никаких секретов в слоях (вместо этого secret mount в BuildKit) и SBOM, сгенерированный вместе с образом. В registry: сканируйте каждый образ на известные уязвимости с помощью Trivy, Grype или Docker Scout, роняйте пайплайн на критических находках, для которых есть исправление, и подписывайте образы, например через cosign, чтобы можно было доказать, что образ пришёл из вашего CI. В рантайме: деплой по digest, а не по изменяемому тегу, запуск без root, с урезанными capabilities и read-only файловой системой, и пересборка по расписанию, чтобы патчи базового образа приходили, даже когда ваш код не меняется. Компромисс — шум: сканеры сообщают о множестве неисправимых или нерелевантных CVE, поэтому нужна чёткая политика того, что блокирует релиз.

S4Можно ли деплоить через Docker Compose на один сервер без даунтайма?

Не из коробки: docker compose up пересоздаёт изменённый контейнер, останавливая старый и запуская новый, поэтому возникает разрыв в несколько секунд, пока приложение стартует. Обойти это можно несколькими способами. Запустите две реплики приложения за nginx и обновляйте их по одной, дожидаясь health check каждой, прежде чем переходить к следующей. Или используйте blue-green пару сервисов: запустите простаивающий «цвет» с новым образом, проверьте его health endpoint, затем переключите upstream в nginx и сделайте reload, который проходит graceful. Инструменты вроде Kamal или плагина docker-rollout автоматизируют ровно этот паттерн. Что бы вы ни выбрали, приложению нужны graceful shutdown, настоящий readiness endpoint и миграции, совместимые с обеими версиями, потому что какое-то время старые и новые контейнеры обслуживают трафик одновременно.

Глоссарий

Namespace
Механизм ядра, который даёт процессу собственное представление о файлах, сети, пользователях или ID процессов, изолируя его от остальных.
cgroup
Механизм ядра, который ограничивает и измеряет, сколько CPU, памяти и I/O может использовать группа процессов.
OCI
Open Container Initiative — открытые стандарты форматов образов и runtime, чтобы разные инструменты могли запускать одни и те же образы.
containerd
Daemon контейнерного runtime, который скачивает образы и управляет жизненным циклом контейнеров; его используют и Docker, и Kubernetes.
runc
Низкоуровневая утилита, которая непосредственно создаёт процесс контейнера с namespace и cgroup по спецификации OCI runtime.
Overlay filesystem
Файловая система, которая накладывает read-only слои образа и один слой для записи так, что они выглядят единым деревом каталогов.
Image digest
sha256-хеш содержимого образа; в отличие от тега, он никогда не сможет позже указывать на другое содержимое.
BuildKit
Современный движок сборки Docker: параллельные стадии, cache и secret mount, удалённые кэши и attestation.
PID 1
Первый процесс в контейнере; у него нет обработки сигналов по умолчанию, и он должен собирать zombie-процессы.
Rootless mode
Запуск Docker daemon и контейнеров от обычного пользователя, чтобы побег из контейнера не давал root на хосте.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.