kai.kzLab

Вопросы на собеседовании по Docker: 15 ситуаций от слоёв до OOM

Команда KAI Lab · · 12 мин чтения

На собеседовании по Docker редко просят рассказать, что такое контейнер. Спрашивают ситуации: образ весит полтора гигабайта, сборка каждый раз ставит зависимости заново, токен попал в образ, контейнер не видит базу, процесс убит с кодом 137.

Ниже 15 таких вопросов по темам: слои и кэш, секреты, multi-stage, сигналы, сети, данные и ресурсы. У каждого — что хочет услышать интервьюер и развёрнутый ответ под спойлером. Все ответы сверены с документацией Docker. Вопросы по остальным темам DevOps — в большой подборке из 41 ситуации.

Образы и слои

1. Образ приложения весит 1,5 ГБ. Откуда столько и как уменьшить?

Что хочет услышать интервьюер: как посмотреть, из чего состоит образ, и три-четыре способа его сократить.

Ответ

docker history image показывает размер каждого слоя, и обычно виновник виден сразу. Типичные причины: полный образ ОС вместо slim-варианта, компиляторы и dev-зависимости, которые нужны только для сборки, кэши пакетных менеджеров и файлы, удалённые в следующем слое. Лечится так: multi-stage сборка, где в финальный образ попадает только результат; slim или distroless база; чистка кэша в той же инструкции RUN, где ставились пакеты; .dockerignore, чтобы в контекст не попадали .git и локальные зависимости.

2. Большой архив удалили в следующем RUN, а образ не уменьшился. Почему?

Что хочет услышать интервьюер: понимание, что слои неизменяемы и складываются стопкой.

Ответ

Каждая инструкция RUN, COPY и ADD добавляет слой поверх предыдущих. Удаление в новом слое только помечает файл как удалённый, а сам файл остаётся в нижнем слое, и этот слой скачивается вместе с образом. Поэтому скачать, использовать и удалить нужно в одной инструкции:

# Плохо: архив остаётся в слое первой инструкции
RUN curl -fsSLo /tmp/tool.tgz https://example.com/tool.tgz
RUN tar -xzf /tmp/tool.tgz -C /opt && rm /tmp/tool.tgz

# Хорошо: временный файл не попадает ни в один слой
RUN curl -fsSLo /tmp/tool.tgz https://example.com/tool.tgz \
 && tar -xzf /tmp/tool.tgz -C /opt \
 && rm /tmp/tool.tgz

Второй путь — multi-stage: всё тяжёлое остаётся в стадии сборки.

3. Каждая сборка заново ставит все зависимости, хотя поменяли одну строку кода. Что не так?

Что хочет услышать интервьюер: как работает кэш сборки и правильный порядок инструкций.

Ответ

По документации Docker, если слой изменился, все слои после него пересобираются, даже если результат был бы тем же. Если COPY . . стоит перед установкой зависимостей, любая правка кода сбрасывает кэш установки. Правильно сначала копировать только манифесты зависимостей:

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
CMD ["node", "server.js"]

Кэш-монтирование BuildKit (--mount=type=cache) сохраняет загруженные пакеты между сборками, даже когда слой пересобирается. .dockerignore не даёт node_modules и .git попадать в контекст и сбивать кэш.

4. В проде все образы с тегом latest. Чем это плохо?

Что хочет услышать интервьюер: тег изменяем; нужны неизменяемые теги или digest.

Ответ

latest — просто тег по умолчанию, который переезжает на каждую новую сборку. По нему нельзя понять, какая версия реально работает. Разные серверы могут скачать разные образы под одним тегом, а откатиться «на прошлый latest» невозможно. Правильно тегировать образ SHA коммита или версией и деплоить по нему. Для полной неизменяемости — ссылаться по digest: registry.example.com/app@sha256:.... Тогда в деплое записано ровно то, что проверили тесты.

Секреты в образе

5. Токен для приватного npm-репозитория передают в сборку через ARG. Что может пойти не так?

Что хочет услышать интервьюер: аргументы сборки видны в образе; как этим воспользовались в атаке на Codecov; как передавать секреты правильно.

Ответ

Документация Docker предупреждает, что аргументы сборки видны в docker history и в provenance-аттестациях, а аргументы и переменные окружения вообще не подходят для секретов: они остаются в итоговом образе. Файл с секретом, удалённый в следующем слое, остаётся в предыдущем.

Так и случилось с Codecov. Атакующий достал HMAC-ключ сервисного аккаунта Google Cloud Storage из промежуточного слоя публичного образа Codecov Self-Hosted. С 31 января 2021 года он периодически подменял скрипт Bash Uploader, и скрипт отправлял на сторонний сервер переменные окружения из CI клиентов — токены, ключи, пароли. 1 апреля подмену заметил клиент: контрольная сумма скачанного скрипта не совпала с опубликованной на GitHub. После этого Codecov сменила все ключи и перевела публичные образы на multi-stage и squash-сборки. Подробнее — в разборе взлома Codecov.

Правильно передавать секрет через secret mount: он доступен только во время одной инструкции и не попадает в слои.

docker build --secret id=npmrc,src=$HOME/.npmrc -t app .
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

Если токен уже попал в образ, удалить образ мало: токен нужно отозвать и выпустить новый.

6. Как проверить, что в готовом образе нет секретов?

Что хочет услышать интервьюер: где секреты прячутся в образе и чем их искать.

Ответ

Смотрю три места. История: docker history --no-trunc image покажет команды сборки вместе с аргументами. Конфигурация: docker image inspect image — переменные окружения в Config.Env. Файлы во всех слоях, а не только в итоговой файловой системе: docker save image -o image.tar и разбор архива или утилита dive, которая показывает содержимое каждого слоя. Вручную это делают разово, а постоянно — сканером в CI: например, Trivy умеет искать в образах не только уязвимости, но и секреты. И отдельно — сканер секретов в самом репозитории, до сборки.

Multi-stage

7. Зачем нужна multi-stage сборка? Покажи пример.

Что хочет услышать интервьюер: разделение окружения сборки и выполнения и что это даёт.

Ответ

В первой стадии есть всё для сборки: компилятор, dev-зависимости, исходники. В финальную стадию копируется только результат через COPY --from. Образ получается меньше, в нём меньше пакетов с уязвимостями, а то, что было нужно только для сборки, в том числе временные файлы, в итоговый образ не попадает.

FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
USER nonroot
ENTRYPOINT ["/app"]

Именно multi-stage и squash-сборки Codecov назвала защитой от атак через слои: промежуточные слои с чувствительными данными не попадают в итоговый образ.

Запуск и сигналы

8. docker stop всегда ждёт 10 секунд, и только потом контейнер останавливается. Почему?

Что хочет услышать интервьюер: SIGTERM, таймаут и SIGKILL; shell-форма и PID 1.

Ответ

docker stop посылает главному процессу SIGTERM, а если тот не завершился за таймаут — SIGKILL. Таймаут по умолчанию для Linux-контейнеров — 10 секунд. Если остановка всегда занимает ровно 10 секунд, процесс не реагирует на SIGTERM.

Частая причина — shell-форма запуска. Документация Docker описывает это для ENTRYPOINT: в shell-форме команда стартует как подпроцесс /bin/sh -c, который не передаёт сигналы, поэтому приложение не становится PID 1 и не получает SIGTERM от docker stop. Решения:

  • exec-форма: CMD ["node", "server.js"] вместо CMD node server.js;
  • в скрипте-обёртке последней командой exec "$@", чтобы приложение заменило shell;
  • docker run --init — маленький init-процесс, который передаёт сигналы и собирает зомби;
  • само приложение должно корректно завершаться по SIGTERM: дописать запросы, закрыть соединения.

В Kubernetes та же логика, только ожидание по умолчанию — 30 секунд.

9. Контейнер ушёл в бесконечный перезапуск. Как разобраться?

Что хочет услышать интервьюер: код выхода, логи, inspect и отладка без рестартов.

Ответ

docker ps -a покажет статус Restarting и код последнего выхода, docker logs --tail 100 app — последние строки перед падением. docker inspect app --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}' добавит, не убит ли он за память и сколько раз перезапускался. Коды: 1 — ошибка приложения, 126 и 127 — команду нельзя выполнить или она не найдена (опечатка в CMD, скрипт без прав на запуск, переводы строк Windows), 137 — SIGKILL, 139 — ошибка сегментации. Для отладки запускаю образ без политики перезапуска и с shell: docker run --rm -it --entrypoint sh image. Подробнее — контейнер Docker перезапускается.

Сети

10. Приложение в контейнере подключается к localhost:5432 и не видит базу, хотя она запущена в соседнем контейнере. Почему?

Что хочет услышать интервьюер: у каждого контейнера своя сеть; DNS по именам в пользовательских сетях.

Ответ

localhost внутри контейнера — сам контейнер: у каждого своё сетевое пространство имён. К базе нужно обращаться по имени. На пользовательской bridge-сети Docker есть встроенный DNS, и контейнеры находят друг друга по имени. На стандартной сети bridge имена не резолвятся: там только IP-адреса или устаревший --link. Docker Compose сам создаёт сеть проекта, и сервисы доступны по имени сервиса: postgres://db:5432. Ещё одна ловушка — depends_on сам по себе ждёт только запуска контейнера базы, а не её готовности. Для готовности нужен healthcheck и condition: service_healthy.

11. Базу опубликовали через -p 5432:5432, на сервере включён ufw с закрытым портом, а база доступна из интернета. Как так?

Что хочет услышать интервьюер: как работает публикация портов и почему она обходит ufw.

Ответ

Документация Docker прямо говорит, что Docker и ufw несовместимы. Трафик к опубликованным портам Docker перенаправляет в таблице nat раньше, чем пакеты доходят до цепочек INPUT и OUTPUT, которые использует ufw, поэтому правила ufw к нему не применяются. Что делать: не публиковать порт базы вообще, если к ней ходят только другие контейнеры, — им хватит общей сети. Если доступ с хоста нужен, публиковать на localhost: -p 127.0.0.1:5432:5432. Снаружи порт можно закрыть ещё и группой безопасности облака или внешним фаерволом. Заодно помнить, что EXPOSE в Dockerfile ничего не публикует — это документация.

Данные

12. Где хранить данные базы в контейнере? Что переживёт docker rm, docker compose down и down -v?

Что хочет услышать интервьюер: слой контейнера, тома и bind mount; что удаляет каждая команда.

Ответ

Записываемый слой контейнера исчезает вместе с ним после docker rm, поэтому данные базы хранят в томе или в каталоге хоста через bind mount. docker compose down по документации удаляет контейнеры сервисов и сети из файла Compose, а именованные тома оставляет. docker compose down -v удаляет и именованные тома из раздела volumes, и анонимные тома контейнеров — после этой команды данных базы больше нет. Тома, объявленные как external, Compose не удаляет никогда. И главное: том — не бэкап. Бэкап — это копия в другом месте, из которой вы проверяли восстановление.

13. Контейнер много пишет в stdout, и на сервере кончился диск. Что настроить?

Что хочет услышать интервьюер: драйвер логов по умолчанию не ротирует файлы.

Ответ

По умолчанию Docker использует драйвер json-file, и, как пишет документация, ротации у него по умолчанию нет: логи болтливого контейнера растут, пока не кончится диск. Решение — ротация в /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Документация рекомендует и драйвер local: он ротирует логи по умолчанию. Важно: после перезапуска Docker настройки применяются только к новым контейнерам, существующие нужно пересоздать. Как найти, что съело место, — в статье No space left on device в Docker.

Ресурсы и OOM

14. Контейнер завершился с кодом 137. Это всегда нехватка памяти?

Что хочет услышать интервьюер: 137 = 128 + 9, то есть SIGKILL, и у него несколько источников.

Ответ

Код 137 значит, что процесс получил SIGKILL. Причин несколько: OOM killer — контейнер превысил свой лимит памяти или кончилась память на хосте; docker kill; docker stop, после которого процесс не уложился в таймаут. Различаю так: docker inspect app --format '{{.State.OOMKilled}}' вернёт true, если контейнер убит за превышение памяти, а в dmesg -T будут сообщения ядра об OOM. Если OOM — смотрю, растёт ли память постоянно (утечка) или скачками (нагрузка, размер кучи JVM или Node.js больше лимита). Подробнее — OOMKilled и exit code 137.

15. Один контейнер съел всю память сервера, и пострадали соседи. Как ограничить ресурсы?

Что хочет услышать интервьюер: лимиты по умолчанию не заданы; что происходит при превышении памяти и CPU.

Ответ

По документации Docker у контейнера по умолчанию нет ограничений: он может взять столько памяти и CPU, сколько отдаст ядро. Лимиты задают при запуске: --memory=512m — жёсткий предел памяти, --memory-reservation — мягкий, --cpus=1.5 — доля процессорного времени. В Compose — deploy.resources.limits. При превышении памяти ядро убивает процессы контейнера, при упоре в CPU контейнер просто замедляется. Docker понижает шанс OOM-kill для самого демона, но не для контейнеров, поэтому без лимитов под удар попадают случайные соседи. Приложению нужно знать свой лимит: например, размер кучи JVM или Node.js настраивают под память контейнера.

Как готовиться

Лучшая подготовка — собрать свой образ и специально его сломать: положить токен в ARG и найти его в docker history, удалить файл в отдельном слое и посмотреть на размер, запустить приложение в shell-форме и замерить docker stop. Такие эксперименты запоминаются лучше списка вопросов. Дальше логичный шаг — вопросы по Kubernetes, где те же контейнеры живут в подах. Если в процессе найма дают домашнее задание с Dockerfile, пригодится статья про тестовое задание DevOps.

Проверить, как вы действуете в похожих ситуациях, можно в бесплатном тесте навыков DevOps: 18 рабочих ситуаций, 10–15 минут, без регистрации.

Частые вопросы

Какие вопросы по Docker задают чаще всего? Про слои и кэш сборки, размер образа, секреты, сети между контейнерами, тома и данные, коды выхода и OOM. Всё чаще — про безопасность: секреты в слоях, запуск не от root, сканирование образов.

Нужно ли знать Docker Swarm? Зависит от компании: в большинстве вакансий оркестратор — Kubernetes. Но базовые вопросы по Docker остаются теми же, потому что поды в Kubernetes запускают те же образы.

Чем отличаются CMD и ENTRYPOINT? ENTRYPOINT задаёт исполняемый файл, CMD — аргументы по умолчанию или команду, если ENTRYPOINT нет. Аргументы docker run заменяют CMD, а ENTRYPOINT переопределяется флагом --entrypoint. Обе инструкции лучше писать в exec-форме, чтобы приложение получало сигналы.

Как не хранить секреты в образе, если они нужны приложению при запуске? Передавать их во время выполнения, а не сборки: переменные окружения или файлы из хранилища секретов, Kubernetes Secrets, Docker secrets. Образ должен быть одинаковым для всех окружений, а секреты и конфигурация — приходить снаружи.

Источники

Читать дальше