--- name: dockerfile-skill description: Ты ОБЯЗАН использовать этот скилл, если хочешь писать Dockerfile --- ## Multistage Используй Docker multistage, если тебе надо создать Dockerfile для сборки и запуска приложения – раздели его на стейдж со сборкой и запуском. Не стесняйся использовать `COPY FROM`. Объединяй несколько команд `RUN` в одну строку с помощью `&&` и очищай временные файлы внутри того же слоя (например, `rm -rf /var/lib/apt/lists/*`). Это уменьшает число слоёв и размер финального образа. Для зависимостей, которые часто пересобираются (pip, npm, apt), используй `--mount=type=cache,target=/root/.cache/pip` (или аналогично) – это ускоряет повторные сборки за счёт кэширования на хосте. ## Parallel Разделяй стейджи Dockerfile таким образом, чтобы они могли выполняться параллельно (например, сборка зависимостей и подготовка тестовых данных). При использовании BuildKit можно объявить несколько независимых `FROM` и копировать между ними через `COPY --from=...`. Убедись, что стейджи не имеют неявных зависимостей друг от друга. ## Makefile Если в проекте есть Makefile, постарайся использовать его. Если Makefile нет, сделай его и вызывай команды через таргеты, а не напрямую в Dockerfile. Также добавь в этот Makefile таргеты для сборки и запуска текущего приложения в Docker. ## Compose Если его еще нет, сделай возможность запустить приложение, для которого пишешь Dockerfile, в Docker Compose. ## Security Используй лучшие практики безопасности при написании Dockerfile. * **Фиксируй версии базовых образов** – всегда используй конкретный тег (например, `python:3.11-slim-bookworm`) вместо `:latest` или плавающих тегов, чтобы обеспечить воспроизводимость сборки. * **Создавай выделенного непривилегированного пользователя** внутри образа: `RUN addgroup -S app && adduser -S -G app app`. Затем переключайся на него с помощью `USER app`. Копируй файлы командой `COPY --chown=app:app`, чтобы они сразу принадлежали этому пользователю, избегая лишних операций с правами. * **Не храни секреты в переменных окружения образа** – используй `ARG` только для нечувствительных данных (например, версий пакетов), а секреты передавай через `--build-arg` в момент сборки или монтируй через Docker Secrets в рантайме. * **Запускай контейнер rootless**, если это возможно. После создания пользователя и переключения на него финальный процесс будет работать без прав root. * **Добавь `HEALTHCHECK`** (например, `HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/ || exit 1`), чтобы оркестраторы могли корректно определять состояние приложения. * **Проставляй метаданные через `LABEL`** – например, `LABEL maintainer="team@example.com"`, `LABEL version="1.0.0"`, `LABEL description="..."`. Это стандарт для документирования образов. * **Указывай в Dockerfile флаги уменьшения размера зависимостей** – для `apt` используй `--no-install-recommends`, для `pip` – `--no-cache-dir`, для `npm` – `--only=production`, для `composer` – `--no-dev`. * **Сканируй собранный образ на уязвимости** – в CI/CD используй инструменты вроде `trivy`, `docker scout` или `grype`. Это должно быть частью пайплайна сборки. ## Dockerignore *Создай файл `.dockerignore` в корне проекта. Включи в него все ненужные файлы и директории: `.git`, `node_modules`, `venv`, `__pycache__`, `*.md`, `docker-compose.yml`, временные файлы. Это ускорит сборку и предотвратит случайную утечку секретов.* ## Пример корректного Dockerfile (мини-шаблон) *Для быстрого старта используй команду `docker init`, которая сгенерирует корректные Dockerfile, compose и .dockerignore под твой язык.* Хорошо, вот интеграция примеров в раздел **Entrypoint** исходного скилла. Я добавил два конкретных скрипта (Alpine/`su-exec` и Ubuntu/`gosu`) и описал, как их встроить в Dockerfile. Также учтена exec-форма и рекомендации по сигналам. ## Entrypoint Сделай кастомный Docker-Entrypoint, который запускает все скрипты из папки `/etc/docker-custom-init/*.sh`, сортируя их по имени. Если нужно запустить само приложение, вызови `app`. Тогда entrypoint должен заменить вызов вызовом оригинального приложения. **Обязательно используй exec-форму** в `ENTRYPOINT` и `CMD` – запись вида `["executable", "param"]`, а не `executable param`. Только exec-форма гарантирует, что сигналы (SIGTERM, SIGINT) будут корректно переданы основному процессу и контейнер сможет правильно завершиться. ### Пример для Alpine (использует `su-exec`) Скрипт `docker-entrypoint.sh` (универсален для Alpine, BusyBox, минимальных образов): ```sh #!/bin/sh set -e INIT_DIRS="${DOCKER_INITD_DIRS:-/etc/init-custom-docker.d}" SCRIPTS_ENVS="${DOCKER_INIT_SCRIPTS_ENVS}" INIT_SCRIPT="${DOCKER_INIT_SCRIPT}" log() { echo "[init] $1" } # 1. Run init scripts from directories (as root) for dir in $(echo "$INIT_DIRS" | tr ',' ' '); do if [ -d "$dir" ]; then log "Processing init directory: $dir" for script in $(find "$dir" -maxdepth 1 -name '*.sh' -type f 2>/dev/null | sort); do log "Running script: $script" sh "$script" done else log "Directory not found, skipping: $dir" fi done # 2. Run scripts from environment variables (as root) if [ -n "$SCRIPTS_ENVS" ]; then for env_name in $(echo "$SCRIPTS_ENVS" | tr ',' ' '); do eval "script_content=\$$env_name" if [ -z "$script_content" ]; then log "Error: Environment variable $env_name specified but empty" exit 1 fi log "Running scripts from environment: $env_name" echo "$script_content" | sh done fi # 3. Run script from DOCKER_INIT_SCRIPT (as root) if [ -n "$INIT_SCRIPT" ]; then log "Running script from DOCKER_INIT_SCRIPT" echo "$INIT_SCRIPT" | sh fi # Switch to appuser using su-exec log "Switching to user: appuser" log "Starting application: $@" exec su-exec appuser:appgroup "$@" ``` Как добавить в Dockerfile (Alpine): ```dockerfile RUN apk add --no-cache su-exec # для Alpine RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -D appuser COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["docker-entrypoint.sh"] CMD ["app"] ``` ### Пример для Ubuntu/Debian (использует `gosu`) Скрипт `docker-entrypoint.sh` (аналог для Debian-based образов): ```bash #!/bin/bash set -e INIT_DIRS="${DOCKER_INITD_DIRS:-/etc/init-custom-docker.d}" SCRIPTS_ENVS="${DOCKER_INIT_SCRIPTS_ENVS}" INIT_SCRIPT="${DOCKER_INIT_SCRIPT}" APP_USER="${APP_USER:-appuser}" APP_GROUP="${APP_GROUP:-appgroup}" log() { echo "[init] $1" } # 1. Execute scripts from directories (as root) IFS=',' read -ra dirs <<< "$INIT_DIRS" for dir in "${dirs[@]}"; do if [ -d "$dir" ]; then log "Processing init directory: $dir" for script in $(find "$dir" -maxdepth 1 -name '*.sh' -type f 2>/dev/null | sort); do log "Running script: $script" bash "$script" done else log "Directory not found, skipping: $dir" fi done # 2. Execute scripts from environment variables (as root) if [ -n "$SCRIPTS_ENVS" ]; then IFS=',' read -ra env_names <<< "$SCRIPTS_ENVS" for env_name in "${env_names[@]}"; do script_content="${!env_name}" if [ -z "$script_content" ]; then log "Error: Environment variable $env_name specified but empty" exit 1 fi log "Running script from environment: $env_name" echo "$script_content" | bash done fi # 3. Execute script from DOCKER_INIT_SCRIPT (as root) if [ -n "$INIT_SCRIPT" ]; then log "Running script from DOCKER_INIT_SCRIPT" echo "$INIT_SCRIPT" | bash fi # Switch to appuser using gosu log "Switching to user: $APP_USER" log "Starting application: $@" exec gosu "${APP_USER}:${APP_GROUP}" "$@" ``` Как добавить в Dockerfile (Ubuntu): ```dockerfile # Установка gosu (два варианта: через apt или копирование статического бинарника) RUN apt-get update && apt-get install -y --no-install-recommends gosu && rm -rf /var/lib/apt/lists/* # Или: COPY --from=gosu/alpine:latest /usr/local/bin/gosu /usr/local/bin/gosu RUN groupadd -r appgroup && useradd -r -g appgroup appuser COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["docker-entrypoint.sh"] CMD ["app"] ``` ### Важные замечания - Внутри скрипта обязательно используй **exec** в конце (например, `exec su-exec appuser "$@"` или `exec gosu appuser "$@"`), чтобы заменить процесс entrypoint на основной процесс приложения. - Пути к init-директориям по умолчанию можно переопределить через переменные окружения (`DOCKER_INITD_DIRS`). Убедись, что нужные директории существуют (создай их в Dockerfile или смонтируй как тома). - Чтобы уменьшить количество слоёв, можно объединить команды `RUN groupadd ... && useradd ... && apt-get install ...` в один `RUN`. # Self-check список для валидации Dockerfile Используй этот список для проверки своего Dockerfile перед финальной сборкой. Отмечай каждый пункт как выполненный. ## 1. Структура и многоступенчатость (Multistage) - [ ] Есть ли разделение на этап сборки и этап запуска (если приложение требует компиляции/установки зависимостей)? - [ ] Используется ли `COPY --from=` для переноса артефактов между стейджами? - [ ] Объединены ли команды RUN в один слой (через `&&`) и очищен ли временный кэш (например, `rm -rf /var/lib/apt/lists/*`)? - [ ] Используется ли `--mount=type=cache` для ускорения повторных сборок (если применимо)? ## 2. Параллелизм (BuildKit) - [ ] Можно ли независимые стейджи выполнять параллельно? (Зависимости, тесты, документация – разделены?) - [ ] Нет ли неявных зависимостей между параллельными стейджами? ## 3. Безопасность - [ ] Версия базового образа фиксирована (например `python:3.11-slim-bookworm`) вместо `:latest`? - [ ] Создан непривилегированный пользователь (`useradd -r -g appgroup appuser`)? - [ ] Финальный процесс запускается от этого пользователя (`USER appuser`)? - [ ] Файлы скопированы с `--chown=appuser:appgroup`? - [ ] Используются флаги уменьшения размера: `--no-install-recommends` (apt), `--no-cache-dir` (pip), `--only=production` (npm)? - [ ] Нет секретов в переменных `ENV` или слоях образа (используются `ARG`/секреты BuildKit)? - [ ] Добавлен `HEALTHCHECK` для продакшен-контейнеров? - [ ] Присутствуют метаданные `LABEL` (maintainer, version, description)? ## 4. Entrypoint - [ ] Написан кастомный скрипт entrypoint, который запускает init-скрипты из `/etc/docker-custom-init/*.sh` (или из переменных окружения)? - [ ] Entrypoint и CMD записаны в exec-форме (`["entrypoint.sh", "param"]`)? - [ ] Внутри скрипта используется `exec` для передачи управления основному процессу (сигналы корректно обрабатываются)? - [ ] Пользователь переключается на непривилегированного (через `gosu` или `su-exec`) в entrypoint? ## 5. .dockerignore - [ ] Существует файл `.dockerignore` в корне проекта? - [ ] В него включены: `.git`, `node_modules`, `venv`, `__pycache__`, `*.md`, `docker-compose.yml`, временные файлы? ## 6. Makefile - [ ] Есть Makefile с таргетами для сборки, запуска, возможно `docker init`? - [ ] В Dockerfile или инструкциях не используется прямая сборка через `docker build`, а только через `make`? ## 7. Docker Compose - [ ] Есть файл `docker-compose.yml` (или `docker-compose.yaml`) для локального запуска? - [ ] В compose корректно указаны порты, volumes, переменные окружения, не используется непривилегированный пользователь без необходимости? ## 8. Сканирование уязвимостей - [ ] В CI/CD добавлен этап сканирования готового образа (trivy, docker scout, grype)? - [ ] Уязвимости критического уровня отсутствуют или задокументированы? ## 9. Тестирование - [ ] Dockerfile успешно собирается без ошибок? - [ ] Контейнер запускается и отвечает на healthcheck (если задан)? - [ ] Init-скрипты выполняются в правильном порядке? --- ### Пример быстрой проверки (через команды) ```bash # 1. Проверка на секреты (не должно быть hardcoded паролей) grep -n 'password\|secret\|key=' Dockerfile # 2. Проверка exec-формы grep -n '^ENTRYPOINT\|^CMD' Dockerfile | grep -v '^\[' # 3. Проверка наличия пользователя grep -n '^RUN.*useradd\|^USER' Dockerfile # 4. Проверка healthcheck grep 'HEALTHCHECK' Dockerfile || echo "HEALTHCHECK отсутствует" ```