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
Для 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 | Что проверяет |
|---|---|
| DL3008 | Pin apt versions |
| DL3009 | rm /var/lib/apt/lists/* |
| DL3015 | apt-get install —no-install-recommends |
| DL3020 | COPY вместо ADD |
| DL3007 | Не FROM image:latest |
| DL3003 | WORKDIR вместо cd |
| DL3025 | JSON-форма CMD/ENTRYPOINT |
| DL3042 | pip install —no-cache-dir |
| DL3047 | wget с —progress=dot:giga |
| DL4006 | bash 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
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 — это первая линия проверки, не единственная.
Попробуй сам
- Создай Dockerfile из «плохого примера» выше. Запусти:
Прочитай все 8 нарушений.docker run --rm -i hadolint/hadolint < Dockerfile - Чини по одному, перезапуская hadolint после каждой правки. Цель — 0 нарушений.
- Добавь
.hadolint.yaml:
Запустиignored: [DL3008]docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml hadolint/hadolint < Dockerfile. DL3008 должен исчезнуть из вывода. - Поставь pre-commit (
pip install pre-commit), создай.pre-commit-config.yamlс hadolint, сделайpre-commit install. Попробуйgit commitс плохим Dockerfile — pre-commit заблокирует. - Бонус: посмотри полный список правил на https://github.com/hadolint/hadolint/wiki. Найди три правила, которые ты не знал — это потенциально новые ошибки в твоих образах.