Хороший образ
Простой Dockerfile из прошлого урока работает. Но в команде и в продакшене нужны ещё пять привычек. Они делают сборку быстрой, образ безопасным, а результат повторяемым.
1. Порядок слоёв и кэш
Заголовок раздела «1. Порядок слоёв и кэш»Docker кэширует слои. Если инструкция и её входные файлы не изменились, Docker берёт готовый слой. Если слой изменился, все слои ниже собираются заново.
Отсюда правило: то, что меняется редко — выше. То, что меняется часто — ниже.
Плохо: сначала весь код, потом зависимости.
COPY app/ .RUN pip install -r requirements.txtВы поменяли одну строку в report.py. Docker заново ставит все пакеты. Это минуты.
Хорошо: сначала список зависимостей, потом код.
COPY app/requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app/ .Теперь правка кода не трогает слой с пакетами.
Смотрим кэш
Заголовок раздела «Смотрим кэш»Мы соберём образ с зависимостями. Скопируйте Dockerfile из раздела «Итоговый Dockerfile» ниже в папку ~/docker-practice и соберите его:
docker build -t orders-app:1.0 .Измените код и соберите снова:
echo "# правка" >> app/report.pydocker build --progress=plain -t orders-app:1.0 .В выводе найдите слова CACHED. Для слоёв RUN useradd, WORKDIR, COPY app/requirements.txt и RUN pip install вы увидите CACHED. Заново собирается только COPY app/ .. Сборка занимает меньше секунды.
Теперь измените requirements.txt:
echo "# правка" >> app/requirements.txtdocker build --progress=plain -t orders-app:1.0 .Слой COPY app/requirements.txt и все слои ниже собираются заново, включая pip install. Слои выше остаются в кэше.
Уберите ваши правки из файлов app/report.py и app/requirements.txt (удалите последнюю строку-комментарий).
2. .dockerignore
Заголовок раздела «2. .dockerignore»При сборке Docker отправляет в сборку всю папку (контекст). Файл .dockerignore говорит, что отправлять не нужно. Формат такой же, как у .gitignore.
Создайте .dockerignore в папке ~/docker-practice:
.git__pycache__*.pyc.envdataЗачем:
- Меньше контекст и быстрее сборка.
- Секреты (
.env) не попадут в образ даже по ошибке. dataне нужна в образе: мы подключаем её через-v.
Проверим защиту. Создайте .env и попробуйте скопировать его:
echo "SECRET=1" > .envprintf 'FROM alpine:3.22\nCOPY .env /app/.env\n' > Dockerfile.baddocker build -f Dockerfile.bad -t orders-bad .ERROR: failed to build: failed to solve: failed to compute cache key: ... "/.env": not foundDocker не нашёл .env, потому что .dockerignore его скрыл. Удалите пробные файлы: rm .env Dockerfile.bad.
3. Не запускайте от root
Заголовок раздела «3. Не запускайте от root»По умолчанию процесс в контейнере работает как root. Если в программе есть уязвимость, у атакующего больше прав. Создайте обычного пользователя:
RUN useradd --create-home appuser...USER appuserПроверьте:
docker run --rm orders-app:1.0 iduid=1000(appuser) gid=1000(appuser) groups=1000(appuser)Пользователя создаём рано, сразу после FROM. Его слой почти не меняется и всегда берётся из кэша. Инструкцию USER ставим после установки пакетов: pip install ещё должен работать от root.
4. Фиксируйте версии
Заголовок раздела «4. Фиксируйте версии»Всё, что может измениться, закрепляйте:
| Что | Плохо | Хорошо |
|---|---|---|
| Базовый образ | python:latest | python:3.12-slim |
| Пакет Python | psycopg | psycopg[binary]==3.3.8 |
| Инструмент | pip install dbt-postgres | pip install dbt-core==1.12.6 dbt-postgres==1.11.0 |
Настоящий пример с dbt. Мы поставили pip install dbt-postgres==1.11.0 и проверили версию:
Core: - installed: 1.13.0-b1 - latest: 1.12.6 - Ahead of latest version!Версия 1.13.0-b1 — это бета. Пакет dbt-postgres требует dbt-core>=1.8.0rc1, и pip взял самую новую версию, даже пре-релиз. Мы закрепили адаптер, но не ядро. Поэтому в образ попала бета.
Вывод: закрепляйте все версии, а не только главный пакет. Для Python — файл requirements.txt с ==.
5. Секреты не кладут в образ
Заголовок раздела «5. Секреты не кладут в образ»В образе всё читается: docker history и docker run образ cat файл. Кто получит образ, получит и всё, что в нём лежит.
Никогда:
COPY .env ...— не копируйте файлы с паролями.ENV DB_PASSWORD=secret— переменная останется в образе навсегда.RUN ... --password secret— пароль виден в истории слоёв.
Передавайте секреты при запуске: -e DB_PASSWORD=... или --env-file. В Compose — через файл .env и секреты оркестратора. Образ хранит программу. Настройки и пароли приходят снаружи.
Multi-stage: когда нужно
Заголовок раздела «Multi-stage: когда нужно»Multi-stage — это сборка в несколько этапов. На первом этапе ставят всё нужное для сборки. На втором берут только результат.
FROM python:3.12-slim AS buildRUN python -m venv /opt/venvCOPY app/requirements.txt .RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slimRUN useradd --create-home appuserCOPY --from=build /opt/venv /opt/venvENV PATH="/opt/venv/bin:$PATH"WORKDIR /appCOPY app/ .USER appuserCMD ["python", "report.py"]Мы проверили, и честный результат такой:
| Образ | Размер |
|---|---|
Обычный Dockerfile | 180 МБ |
| Multi-stage | 183 МБ |
Для нашего приложения выигрыша нет. Здесь пакет psycopg[binary] приходит уже собранным.
Multi-stage окупается, когда для сборки нужны компиляторы (gcc, заголовки C), а в готовом образе они не нужны. Тогда образ может стать на сотни мегабайт меньше. Не усложняйте Dockerfile, пока у вас нет такой задачи.
Теги и публикация (только объяснение)
Заголовок раздела «Теги и публикация (только объяснение)»Чтобы поделиться образом, ему дают имя с адресом реестра и отправляют:
docker tag orders-app:1.0 ghcr.io/<user>/orders-app:1.0docker push ghcr.io/<user>/orders-app:1.0Здесь <user> — ваш логин на GitHub. Перед push нужен вход: docker login ghcr.io. Мы этого не делаем в курсе. Как это работает в команде — в уроке Контейнеры в продакшене.
Теги образов тоже фиксируют. Не перезаписывайте 1.0. Выпустили исправление — это 1.1.
В учебных сборках мы перезаписываем 1.0 для простоты. В команде так не делают.
Итоговый Dockerfile
Заголовок раздела «Итоговый Dockerfile»Это образ orders-app:1.0. Он нужен нам в следующих уроках. Сохраните его в ~/docker-practice/Dockerfile (замените старый):
FROM python:3.12-slim
# Пользователь без прав root. Создаём его рано: этот слой почти не меняетсяRUN useradd --create-home appuser
WORKDIR /app
# Сначала зависимости: слой берётся из кэша, пока requirements.txt не изменилсяCOPY app/requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
# Потом код: он меняется чащеCOPY app/ .
USER appuser
CMD ["python", "report.py"]В образе два скрипта: report.py и load_orders.py. По умолчанию запускается report.py. Второй мы запустим позже, заменив команду в Compose.
Запуск:
docker build -t orders-app:1.0 .docker run --rm -v "$PWD/data:/data:ro" orders-app:1.0Файл: /data/orders.csvЗаказов: 500Выручка по каналам (только completed): organic 23715.82 ads 23082.74 email 15459.67Проверьте, что psycopg есть в образе:
docker run --rm orders-app:1.0 python -c "import psycopg; print(psycopg.__version__)"3.3.8Практика
Заголовок раздела «Практика»Работайте в папке ~/docker-practice.
Задание 1
Заголовок раздела «Задание 1»Создайте .dockerignore и итоговый Dockerfile. Соберите orders-app:1.0 и запустите отчёт.
Проверка: Заказов: 500, organic 23715.82, ads 23082.74, email 15459.67.
Решение
Файлы — выше. Сборка и запуск:
docker build -t orders-app:1.0 .docker run --rm -v "$PWD/data:/data:ro" orders-app:1.0Задание 2
Заголовок раздела «Задание 2»От какого пользователя работает контейнер? Какой у него uid?
Проверка: uid=1000(appuser).
Решение
docker run --rm orders-app:1.0 idЗадание 3
Заголовок раздела «Задание 3»Проверьте кэш. Добавьте в конец app/report.py строку-комментарий и соберите образ с --progress=plain. Какие слои были CACHED? Какой слой пересобрался?
Проверка: CACHED у слоя pip install. Пересобрался только COPY app/ ..
Решение
echo "# правка" >> app/report.pydocker build --progress=plain -t orders-app:1.0 . 2>&1 | grep -E "CACHED|COPY|RUN"Потом удалите комментарий из report.py.
Задание 4
Заголовок раздела «Задание 4»Поменяйте порядок в Dockerfile на плохой: сначала COPY app/ ., потом pip install. Измените report.py и соберите. Что изменилось в кэше? Верните правильный порядок.
Проверка: слой pip install больше не CACHED: пакеты ставятся заново при каждой правке кода.
Решение
Плохой порядок:
COPY app/ .RUN pip install --no-cache-dir -r requirements.txtrequirements.txt уже внутри app/, поэтому отдельный COPY не нужен. Любая правка кода меняет COPY app/ ., а значит, и всё ниже.
Задание 5
Заголовок раздела «Задание 5»Докажите, что секрет в ENV виден всем. Соберите образ с ENV DB_PASSWORD=secret и найдите пароль без запуска программы.
Проверка: пароль виден в выводе docker image inspect.
Решение
printf 'FROM alpine:3.22\nENV DB_PASSWORD=secret\n' > Dockerfile.baddocker build -f Dockerfile.bad -t orders-bad .docker image inspect orders-bad --format '{{.Config.Env}}'docker history orders-badrm Dockerfile.baddocker rmi orders-badВ Env есть DB_PASSWORD=secret. Пароль в образе навсегда. Передавайте его через -e при запуске.
Дальше: Сети.