Files
go-synapse-backupper/.agents/skills/chore/dockerfile-skill/SKILL.md
T

17 KiB
Raw Blame History

name, description
name description
dockerfile-skill Ты ОБЯЗАН использовать этот скилл, если хочешь писать 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, минимальных образов):

#!/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):

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 образов):

#!/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):

# Установка 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-скрипты выполняются в правильном порядке?

Пример быстрой проверки (через команды)

# 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 отсутствует"