Learning Platform
Глоссарий Troubleshooting
Урок 16.03 · 20 мин
Начальный
dockersecuritylinting

Hadolint: shellcheck для Dockerfile

Hadolint — линтер для Dockerfile. Аналог shellcheck для bash, ruff для Python, eslint для JS. Берёт твой Dockerfile, парсит его, проверяет на best practices и потенциальные проблемы. Возвращает список warning’ов / error’ов с конкретными правилами.

Это первый шаг security/quality-аудита образа: до сборки, до сканера CVE, просто на статике файла. Hadolint находит проблемы за миллисекунды и часто экономит часы дебага.


Первый скрипт: shebang, chmod +x, аргументы

Что такое Hadolint

Сделан на Haskell, использует встроенный shellcheck для проверки команд внутри RUN. Правила в формате DL<NNNN> — например, DL3008 (pin apt-get versions), DL3020 (use COPY instead of ADD). Все правила задокументированы в репо: https://github.com/hadolint/hadolint/wiki.

В мае 2026 поддерживается ~70 правил, плюс правила shellcheck для содержимого RUN-команд.


Запуск локально

Самый простой способ — через Docker:

docker run --rm -i hadolint/hadolint < Dockerfile

-i keep STDIN, через < файл шлётся в stdin. Hadolint читает Dockerfile со stdin.

Выход:

-:5 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
-:8 DL3009 info: Delete the apt-get lists after installing something
-:12 DL3020 error: Use COPY instead of ADD for files and folders

Формат: <file>:<line> <rule-id> <severity>: <message>.

Альтернативно — установить локально:

brew install hadolint            # macOS
sudo apt install hadolint        # некоторые дистро Linux
# Или скачать бинарь с https://github.com/hadolint/hadolint/releases

И запускать как обычный CLI:

hadolint Dockerfile
TIP

Для junior — Docker-вариант проще: ничего не устанавливать, всегда последняя версия. Для повседневной работы — лучше локальный бинарь, мгновенный отклик в IDE через расширения (есть для VS Code, IntelliJ, Vim).


Топ-5 правил, с которыми ты столкнёшься

DL3008: Pin apt versions

# BAD:
RUN apt-get update && apt-get install -y curl postgresql-client

# GOOD:
RUN apt-get update && apt-get install -y \
    curl=7.88.* \
    postgresql-client=16.* \
    && rm -rf /var/lib/apt/lists/*

Без pin’а версий сборка не воспроизводима: сегодня curl 7.88, через месяц 7.89, и поведение может измениться. Pin делает образ детерминированным.

Caveat: postgres-client может не быть доступен в нужной точной версии в repo Debian. Тогда используй wildcard (16.*) или принимай non-pinned как осознанный trade-off.

DL3009: Delete apt-get lists

# BAD:
RUN apt-get update && apt-get install -y curl

# GOOD:
RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

После apt-get update в /var/lib/apt/lists/ лежат пакетные индексы (десятки МБ). Они нужны только во время сборки. Удаление в том же RUN-слое уменьшает размер образа.

/var/lib/apt/lists/ не удалится через отдельный RUN rm, потому что слои аккумулируются — файлы из предыдущего слоя останутся в финальном образе. Только удаление в том же RUN помогает.

DL3003: Use WORKDIR not cd

# BAD:
RUN cd /app && python main.py
RUN cd /tmp && wget ...

# GOOD:
WORKDIR /app
RUN python main.py

WORKDIR /tmp
RUN wget ...

cd внутри RUN создаёт subshell, эффект пропадает после команды. WORKDIR сохраняется между всеми последующими RUN/CMD/ENTRYPOINT.

DL3020: Use COPY not ADD

# BAD:
ADD requirements.txt /app/

# GOOD:
COPY requirements.txt /app/

ADD — старая команда с магией: распаковывает .tar.*, скачивает по URL. Эти фичи редко нужны и часто вредят (случайно распаковал архив, который не должен был). COPY делает только то, что заявлено — копирует файлы.

Исключение: если специально хочешь распаковку tar — используй ADD. Если специально скачать по URL — лучше отдельный RUN curl ... (контроль checksums, retries).

DL3007: Don’t use latest tag

# BAD:
FROM postgres:latest
FROM python:latest

# GOOD:
FROM postgres:16.3
FROM python:3.13.0-slim

latest — это «то, что было latest когда я в последний раз pull’ил». Сегодня postgres:latest = 16.3, завтра — 17.0. Сборка ломается, никто не понимает почему. Pin версий — фундамент воспроизводимости.


Все правила одним списком

Hadolint ругается на много разных вещей. Полный список — на wiki, но категории такие:

  • DL3000-DL3099 — синтаксис и best practices Dockerfile.
  • DL4000-DL4099 — производительность, размер образа.
  • SC2000+ — правила shellcheck для команд в RUN.

Топ-10 по частоте срабатывания на код Junior’а:

RuleЧто проверяет
DL3008Pin apt versions
DL3009rm /var/lib/apt/lists/*
DL3015apt-get install —no-install-recommends
DL3020COPY вместо ADD
DL3007Не FROM image:latest
DL3003WORKDIR вместо cd
DL3025JSON-форма CMD/ENTRYPOINT
DL3042pip install —no-cache-dir
DL3047wget с —progress=dot:giga
DL4006bash flags set -o pipefail

Severity

Hadolint имеет 4 уровня severity:

  • error — это блокер, сборку лучше fail’ить.
  • warning — заметная проблема, в дальней перспективе ломает.
  • info — рекомендация.
  • style — косметика, можно игнорить.

В CI можно фейлить только error:

docker run --rm -i hadolint/hadolint --failure-threshold error < Dockerfile

Или error+warning (для строгого режима):

docker run --rm -i hadolint/hadolint --failure-threshold warning < Dockerfile

Игнорирование правил

Бывают случаи, когда правило не подходит. Например, ты намеренно не pin’ишь curl — потому что Dockerfile для CI-образа, тебе нужны последние security-патчи. Можно игнорировать конкретное правило:

docker run --rm -i hadolint/hadolint --ignore DL3008 < Dockerfile

Или через файл конфига .hadolint.yaml:

ignored:
  - DL3008
  - DL3009

trustedRegistries:
  - docker.io
  - gcr.io

trustedRegistries — если Hadolint жалуется на образы из чужих регистров, в этот список добавляются твои доверенные.

Локальное игнорирование строки (как # shellcheck disable=...):

# hadolint ignore=DL3008
RUN apt-get install -y curl

Интеграция в pre-commit

pre-commit — стандартный Python-инструмент для запуска линтеров перед git commit. Hadolint туда добавляется в .pre-commit-config.yaml:

repos:
  - repo: https://github.com/hadolint/hadolint
    rev: v2.13.0
    hooks:
      - id: hadolint
        args: ["--failure-threshold", "warning"]

После pre-commit install каждый git commit проверит изменённые Dockerfile’ы. Если есть ошибки — коммит не пройдёт.

В CI (GitHub Actions):

- name: Lint Dockerfile
  run: docker run --rm -i hadolint/hadolint --failure-threshold error < Dockerfile

Это блокирует merge’и, если PR ломает best practices.

Hadolint в pipeline
DockerfileТекущая версия из репо или PR
hadolintПарсит синтаксис, проверяет 70+ правил, плюс shellcheck для содержимого RUN
error/warning/infoСписок нарушений с номером правила, строкой, severity и сообщением
CI: fail or passС --failure-threshold error фейлим только на критичных, на warning/info — оставляем как заметки

Пример: до и после hadolint

Junior пишет:

FROM python:latest

ADD . /app
RUN cd /app && pip install -r requirements.txt

RUN apt-get update
RUN apt-get install -y curl

CMD python /app/main.py

Hadolint выводит:

-:1 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag.
-:3 DL3020 error: Use COPY instead of ADD for files and folders
-:4 DL3003 warning: Use WORKDIR to switch to a directory
-:4 DL3042 info: Avoid the use of cache directory with pip. Use `pip install --no-cache-dir <package>`
-:6 DL3009 info: Delete the apt-get lists after installing something
-:7 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
-:7 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
-:9 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT arguments

Чиним:

FROM python:3.13-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl=7.88.* \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

CMD ["python", "main.py"]

Запускаем — 0 нарушений. Бонус: размер образа упал процентов на 30 благодаря --no-install-recommends и rm /var/lib/apt/lists/*.


Что Hadolint НЕ ловит

Стоит помнить ограничения:

  • CVE в пакетах. Hadolint не знает, что curl 7.88 имеет уязвимость X. Это работа Trivy / Docker Scout (следующий урок).
  • Логические баги. Если ты COPY секреты — Hadolint не увидит. Это работа secret scanner’ов.
  • Эффективность кэширования. Hadolint не подскажет, что нужно вынести pip install в отдельный слой ДО COPY .. Это знание архитектуры Docker (модуль 8).
  • Безопасность runtime. USER не указан — Hadolint предупредит (правило DL3002), но не оценит, насколько критично.

Hadolint — это первая линия проверки, не единственная.

Stack из 3 линтеров для образа
hadolintСтатика Dockerfile. До сборки. Best practices, синтаксис
docker buildЕсли линт прошёл — собираем образ
trivy / scoutСканирует собранный образ на CVE: уязвимости в системных пакетах и Python-зависимостях
secret scannertrufflehog или gitleaks — ищет токены, ключи, пароли в слоях образа

Попробуй сам

  1. Создай Dockerfile из «плохого примера» выше. Запусти:
    docker run --rm -i hadolint/hadolint < Dockerfile
    Прочитай все 8 нарушений.
  2. Чини по одному, перезапуская hadolint после каждой правки. Цель — 0 нарушений.
  3. Добавь .hadolint.yaml:
    ignored: [DL3008]
    Запусти docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml hadolint/hadolint < Dockerfile. DL3008 должен исчезнуть из вывода.
  4. Поставь pre-commit (pip install pre-commit), создай .pre-commit-config.yaml с hadolint, сделай pre-commit install. Попробуй git commit с плохим Dockerfile — pre-commit заблокирует.
  5. Бонус: посмотри полный список правил на https://github.com/hadolint/hadolint/wiki. Найди три правила, которые ты не знал — это потенциально новые ошибки в твоих образах.

Проверка знанийKnowledge check
Объясни, почему DL3008 (pin apt-get versions) и DL3009 (delete apt-get lists) одновременно ВАЖНЫ для production-образа Python ETL, и что произойдёт если их игнорировать?
ОтветAnswer
DL3008 — pin apt versions (apt-get install curl=7.88.*). Без пина каждая пересборка образа может тянуть разную версию пакета. Сегодня curl 7.88, через месяц 7.89 — поведение изменилось (другие default'ы, другой формат вывода, новые баги). Производственный образ становится не воспроизводимым: сборка успешна в понедельник, ломается в среду без видимых причин. Для ETL это критично — DAG, который работал, начинает падать после rebuild. DL3009 — удалять /var/lib/apt/lists/* в том же RUN-слое. После apt-get update в /var/lib/apt/lists/ лежат индексы пакетов (40-80 МБ для Debian). Они нужны только в момент установки и могут быть удалены сразу. ВАЖНО: удалять в ТОМ ЖЕ RUN, что и установка — Docker аккумулирует слои, если удалить в отдельном RUN, файлы останутся в нижнем слое и в финальном образе. Игнорируя оба: - образ для Python ETL становится 1.5 ГБ вместо 200 МБ (apt-кеш + non-pinned зависимости), - сборка не воспроизводима — CI падает периодически без правок кода, - pull образа на новых хостах занимает минуты вместо секунд (больше байт качать), - registry storage растёт — для команды на 10 человек это +TB за год. Hadolint ловит это за секунды на статике, до полноценной сборки.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Hadolint срабатывает на правило DL3009 'Delete the apt-get lists after installing something'. Что это означает и как починить?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5