Learning Platform
Глоссарий Troubleshooting
Урок 08.02 · 24 мин
Начальный
dockerdockerfilefromruncopyadd

FROM, RUN, COPY: фундаментальные инструкции

В прошлом уроке мы написали 10-строчный Dockerfile. Теперь разберём, что значит каждая из главных инструкций, какие у них варианты и где разработчики чаще всего ошибаются. Эти три (FROM, RUN, COPY) встречаются почти в каждом Dockerfile и определяют размер образа, скорость сборки и удобство сопровождения.

В этом уроке: выбор базового образа для DE, почему один RUN лучше пяти, COPY vs ADD, и базовый .dockerignore для Python-проекта.


FROM: выбор базового образа

FROM это первая (или почти первая) инструкция в Dockerfile. Она задаёт «фундамент», на котором строится твой образ:

FROM <image>:<tag>
FROM <image>@<digest>
FROM <image>:<tag> AS <stage-name>      # для multi-stage (модуль 8)

Для DE есть несколько типичных вариантов:

Базовый образРазмерКогда использовать
python:3.13~1 GBНикогда — слишком жирный, gcc и dev-tools уже встроены
python:3.13-slim~145 MBDE-стандарт — Debian-slim с Python, ничего лишнего
python:3.13-alpine~50 MBМаленький, но musl libc → проблемы с pip wheels (numpy, pandas)
python:3.13-slim-bookworm~145 MBТо же что slim, явно указанная версия Debian (для воспроизводимости)
debian:12-slim~75 MBКогда нужно ставить Python через apt вручную
ubuntu:24.04~80 MBЕсли команда работает с Ubuntu-specific пакетами
gcr.io/distroless/python3-debian12~50 MBDistroless: без shell, минимальная attack surface для prod
scratch0 BДля статически слинкованных Go/Rust бинарников
Дистрибутивы Linux: Debian, RHEL, Arch, Alpine

Для большинства DE-задач python:3.13-slim это правильный default. Он:

  • Достаточно мал (~145 MB), быстро pull’ится
  • Содержит glibc (а не musl), что критично для precompiled wheels — numpy / pandas / scipy не требуют сборки из исходников
  • Имеет apt для установки system deps (libpq-dev, build-essential для C-extensions)
  • Хорошо документирован, mainstream в индустрии
WARNING

Alpine привлекателен размером, но проблема в musl libc. Многие Python wheels precompiled под glibc — при pip install на Alpine они либо не находят wheel и собираются из исходников (медленно, требует gcc + dev-headers), либо несовместимы. Pandas на Alpine = boilerplate из 50+ строк apk add. Slim с glibc = pip install pandas работает мгновенно.

# DE-стандартный FROM
FROM python:3.13-slim

# Если нужна конкретная patch-версия для воспроизводимости
FROM python:3.13.1-slim-bookworm

# Pinning через digest для прода
FROM python@sha256:5b7b9c0f6c9e3e8a4f0d8b6a1e2c3d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1

RUN: один RUN это один слой

RUN выполняет команду в новом контейнере поверх предыдущего слоя и фиксирует результат как новый read-only слой. Базовая форма:

RUN <command>

Это shell form — команда выполняется через /bin/sh -c <command>. Альтернативно есть exec form:

RUN ["executable", "param1", "param2"]

Exec form не использует shell, поэтому не делает variable substitution, не интерпретирует &&, |, redirects. На практике для RUN почти всегда используется shell form, exec form чаще для CMD/ENTRYPOINT (урок 06.03).

Главное правило: объединять связанные команды в один RUN. Не делать так:

# Плохо: 5 слоёв, мусор остаётся в нижних
RUN apt-get update
RUN apt-get install -y curl jq vim
RUN curl -L https://example.com/tool > /usr/local/bin/tool
RUN chmod +x /usr/local/bin/tool
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

Правильно:

# Хорошо: 1 слой, apt-cache и lists очищены ДО snapshot'а
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl jq vim && \
    curl -L https://example.com/tool -o /usr/local/bin/tool && \
    chmod +x /usr/local/bin/tool && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Это даёт:

  • Меньше слоёв (быстрее pull, проще читать docker history)
  • Меньший размер (см. модуль 6: rm в следующем слое не удаляет данные из предыдущего)
  • Атомарность: либо весь сетап получился, либо весь упал — нет «полусобранных» промежуточных слоёв

Ключевые подсказки для RUN с apt:

  • --no-install-recommends не ставит «рекомендованные» пакеты (часто +100MB безполезного)
  • apt-get update без install почти всегда ошибка (lists устаревают между слоями)
  • В конце rm -rf /var/lib/apt/lists/* убирает apt-cache (10-20MB)
  • set -eux (или -o pipefail) делает RUN падающим на любой ошибке (без этого RUN cmd1 && cmd2 падает только если cmd2 не выполнился — cmd1 может тихо упасть)
Объединение RUN: меньше слоёв, меньше wasted space
5 RUN: ~340MBПлохо: 5 отдельных RUN. apt-cache попадает в слой 1, его удаление в слое 5 создаёт whiteout, но 20MB остаются в lower-слое.
оптимизация
1 RUN: ~280MBХорошо: один RUN с && соединением. apt-cache очищен ДО snapshot'а, в образ не попадает.

COPY vs ADD

Обе инструкции копируют файлы из build context в образ. Разница:

COPY простой — копирует файлы и директории. Поддерживает:

  • Wildcards (COPY *.py /app/)
  • chown/chmod (COPY --chown=user:user . /app/)
  • --from=stage для multi-stage build (модуль 8)
  • --link для лучшего кэширования (BuildKit)
# Базовое
COPY requirements.txt /app/

# С wildcards
COPY src/*.py /app/src/

# С правами владельца
COPY --chown=appuser:appgroup ./app /app

# Несколько источников в одну директорию
COPY requirements.txt setup.py /app/

ADD делает то же, но ещё умеет:

  • Распаковывать tar-архивы (ADD app.tar.gz /app/ распакует автоматически)
  • Скачивать URL (ADD https://example.com/file.tar.gz /tmp/)
# Распакует автоматически
ADD app.tar.gz /app/

# Скачает по URL
ADD https://example.com/binary /usr/local/bin/binary

Правило: для копирования файлов всегда используй COPY. ADD только когда нужна auto-extract tar (но даже там лучше явный RUN tar xzf).

Почему COPY лучше ADD:

  • Предсказуемость. ADD автоматически распакует .tar.gz, но не .zip; URL без auth-headers; для git-репо не работает. COPY делает ровно одно — копирует.
  • Cache invalidation. ADD URL не имеет хорошего cache key (Docker не знает, изменилось ли содержимое URL без HEAD-запроса).
  • Безопасность. ADD URL может неожиданно вытянуть что-то незапиннутое.

Современный workflow:

# Плохо: ADD URL
ADD https://example.com/tool.tar.gz /tmp/

# Хорошо: явный RUN curl с pinned URL и checksum
RUN curl -fsSL https://example.com/tool-v1.2.3.tar.gz -o /tmp/tool.tar.gz && \
    echo "abc123...  /tmp/tool.tar.gz" | sha256sum -c - && \
    tar xzf /tmp/tool.tar.gz -C /opt/ && \
    rm /tmp/tool.tar.gz
TIP

Hadolint (линтер для Dockerfile) выдаёт DL3020 “Use COPY instead of ADD for files and folders”. В CI это автоматически предотвращает антипаттерны. Подробнее про hadolint — в уроке 07.05.


.dockerignore: что НЕ копировать

Когда ты пишешь COPY . /app, Docker копирует всё содержимое build context’а. Если в проекте лежит:

  • .git/ (история коммитов, иногда 100+ MB)
  • __pycache__/, *.pyc (Python байткод — не нужен в образе, Python пересоздаст)
  • .venv/, venv/ (виртуальное окружение — не нужно, pip install внутри сделает своё)
  • .pytest_cache/, .mypy_cache/ (кэши тестов и линтеров)
  • node_modules/ (если параллельно есть JS)
  • Локальные *.csv / *.parquet с тестовыми данными
  • .env с secret’ами
  • IDE-файлы: .vscode/, .idea/

— весь этот мусор попадёт в образ. Раздувает размер, рискует утечкой секретов, и инвалидирует cache при изменении любого мусорного файла.

Решение: файл .dockerignore в корне build context’а:

# .dockerignore (типичный для Python DE-проекта)

# Version control
.git
.gitignore
.gitattributes

# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
.venv
venv/
ENV/
env/
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
htmlcov/
*.egg-info/
dist/
build/

# IDE
.vscode
.idea
*.swp
.DS_Store

# Local data
data/
*.parquet
*.csv
*.json.gz

# Secrets (failsafe -- secrets не должны быть в репо вообще)
.env
.env.local
*.pem
*.key

# Docker-specific (избежать рекурсии)
Dockerfile.dev
docker-compose.override.yml

# CI artifacts
.github/
.gitlab-ci.yml

Синтаксис .dockerignore похож на .gitignore:

  • Один pattern на строку
  • # — комментарий
  • * — wildcard для одного компонента пути
  • ** — wildcard для нескольких компонентов (**/__pycache__ — везде где встретится)
  • !pattern — исключение (например *.pem\n!public.pem)
  • path//) — только директория

После добавления .dockerignore build context уменьшается в разы:

$ docker build -t my-etl:v1 .
 => => transferring context: 1.45kB    # с .dockerignore (только нужные файлы)
# vs без .dockerignore:
 => => transferring context: 230MB     # включая .git, .venv, data/, ...

Реалистичный Dockerfile для DE

Собираем всё вместе. ETL-сервис, который читает Postgres и пишет в S3:

# Dockerfile
FROM python:3.13-slim

# System deps (для psycopg2-binary, boto3, и т.п.)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        libpq5 \
        ca-certificates && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Сначала requirements -- кэш сохранится при изменении кода
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Остальной код
COPY . .

# Запуск
CMD ["python", "-m", "etl"]

И сопровождающий .dockerignore:

.git
__pycache__
*.pyc
.venv
.pytest_cache
.mypy_cache
data/
tests/fixtures/
.env

После build:

$ docker build -t my-etl:v1 .
$ docker images my-etl
REPOSITORY  TAG  IMAGE ID         CREATED          SIZE
my-etl      v1   abc123def456     30 seconds ago   195MB

195MB — отличный размер для Python-ETL с psycopg2 и boto3.


Попробуй сам

Сравни эффект разных стратегий RUN:

mkdir bad-good && cd bad-good

# Плохой Dockerfile -- 5 RUN
cat > Dockerfile.bad <<'EOF'
FROM python:3.13-slim
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y jq
RUN curl -L https://github.com/koalaman/shellcheck/releases/download/v0.10.0/shellcheck-v0.10.0.linux.x86_64.tar.xz -o /tmp/sc.tar.xz
RUN apt-get clean
EOF

# Хороший Dockerfile -- 1 RUN с cleanup
cat > Dockerfile.good <<'EOF'
FROM python:3.13-slim
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl jq && \
    curl -L https://github.com/koalaman/shellcheck/releases/download/v0.10.0/shellcheck-v0.10.0.linux.x86_64.tar.xz -o /tmp/sc.tar.xz && \
    rm /tmp/sc.tar.xz && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
EOF

# Сборка обоих
docker build -f Dockerfile.bad -t example:bad .
docker build -f Dockerfile.good -t example:good .

# Сравнение размеров
docker images example
# REPOSITORY  TAG    SIZE
# example     good   ~180MB
# example     bad    ~220MB

# Сравнение количества слоёв
docker history example:bad | wc -l
docker history example:good | wc -l

cd .. && rm -rf bad-good
docker rmi example:bad example:good

Проверка знанийKnowledge check
В Dockerfile FROM python:3.13-alpine. При сборке RUN pip install pandas падает с 'fatal error: cmath.h: No such file or directory'. В чём причина и как исправить (два варианта)?
ОтветAnswer
Alpine использует musl libc, многие precompiled wheels для pandas / numpy / scipy скомпилированы под glibc. Pip не находит совместимый wheel и пытается собрать из исходников -- нужны gcc, g++, dev-headers Python и cmath.h. Решение 1 (правильное): переключиться на FROM python:3.13-slim -- glibc, precompiled wheels устанавливаются мгновенно. Решение 2 (если Alpine обязателен): RUN apk add --no-cache gcc g++ musl-dev python3-dev && pip install pandas -- работает, но build занимает 10+ минут и образ становится больше. В DE-практике почти всегда выбирают slim.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Команда хочет минимизировать размер Python-ETL образа с pandas. Какой базовый образ выбрать?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5