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