Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 22 мин
Начальный
dockerdockerfilebuildpythonbuild-context

Первый Dockerfile: от Python-скрипта до образа

До сих пор мы пользовались чужими образами: pull, run, stop. В этом модуле учимся собирать свои. Dockerfile это текстовый файл с инструкциями, по которому Docker daemon строит образ. Это не магия и не язык программирования — это плоский список команд, каждая из которых создаёт слой (см. модуль 6). В DE-практике Dockerfile это твой главный артефакт: ETL-сервис, Airflow worker, custom Python-инструмент — всё это пакетируется в Dockerfile, версионируется в git, собирается в CI.

В этом уроке: первый Dockerfile для простого Python-скрипта, полный workflow build → run, и что такое build context.


Минимальный Dockerfile для Python

Возьмём простой ETL-скрипт — читает CSV, считает агрегаты, печатает в stdout. Структура проекта:

my-etl/
├── etl.py
├── requirements.txt
└── Dockerfile
# etl.py
import pandas as pd
import sys

if __name__ == "__main__":
    df = pd.read_csv(sys.stdin)
    print(df.groupby('category').agg({'value': ['count', 'sum', 'mean']}))
# requirements.txt
pandas==2.2.3
# Dockerfile
FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY etl.py .

CMD ["python", "etl.py"]

10 строк, и у нас полноценный образ. Что делает каждая инструкция:

  • FROM python:3.13-slim — выбираем базовый образ. python:3.13-slim это Debian-slim с уже установленным Python 3.13 (~145MB). Все следующие инструкции применяются поверх него.
  • WORKDIR /app — устанавливает рабочую директорию для всех последующих команд. Если /app не существует — Docker её создаст.
  • COPY requirements.txt . — копирует файл из build context в текущую WORKDIR (то есть /app/requirements.txt). Точка . это «текущая директория WORKDIR».
  • RUN pip install --no-cache-dir -r requirements.txt — выполняет команду внутри слоя. --no-cache-dir не сохранять wheel’ы в ~/.cache/pip (экономит ~50MB).
  • COPY etl.py . — копирует основной код. Отдельно от requirements, чтобы изменения в etl.py не инвалидировали кэш слоя с pip install (см. урок 07.05 best practices).
  • CMD ["python", "etl.py"] — команда по умолчанию при docker run. Подробно про CMD vs ENTRYPOINT — в уроке 06.03.

docker build: первая сборка

Из директории my-etl/:

$ docker build -t my-etl:v1 .

Что значит каждый кусок:

  • docker build — команда сборки
  • -t my-etl:v1 — tag для собранного образа (имя:версия)
  • .build context: директория, которая отправляется daemon’у как «всё, что доступно для COPY/ADD»

Вывод:

[+] Building 18.5s (10/10) FINISHED                                          docker:default
 => [internal] load build definition from Dockerfile                                 0.0s
 => => transferring dockerfile: 187B                                                 0.0s
 => [internal] load .dockerignore                                                    0.0s
 => => transferring context: 2B                                                      0.0s
 => [internal] load metadata for docker.io/library/python:3.13-slim                  1.8s
 => [internal] load build context                                                    0.1s
 => => transferring context: 1.45kB                                                  0.0s
 => [1/4] FROM docker.io/library/python:3.13-slim@sha256:abc...                      0.0s
 => CACHED [2/4] WORKDIR /app                                                        0.0s
 => [3/4] COPY requirements.txt .                                                    0.1s
 => [4/4] RUN pip install --no-cache-dir -r requirements.txt                        12.4s
 => [5/5] COPY etl.py .                                                              0.1s
 => exporting to image                                                               0.3s
 => => exporting layers                                                              0.3s
 => => writing image sha256:5e8a7b6c...                                              0.0s
 => => naming to docker.io/library/my-etl:v1                                         0.0s

Каждый шаг это инструкция Dockerfile. CACHED означает, что Docker нашёл слой в локальном кэше и использовал его без повторного выполнения (см. урок 07.02 BuildKit cache). При первой сборке CACHED не будет — кэш пуст. При повторной без изменений все шаги станут CACHED, и build займёт секунды.

После build:

$ docker images my-etl
REPOSITORY  TAG    IMAGE ID         CREATED          SIZE
my-etl      v1     5e8a7b6c4d3f     30 seconds ago   175MB

175MB — почти всё это python:3.13-slim (145MB) + pandas (~30MB).


docker run: запуск собранного образа

Базовый запуск:

$ echo "category,value
a,10
a,20
b,30
b,40" | docker run --rm -i my-etl:v1

#           value
#           count sum  mean
# category
# a            2  30  15.0
# b            2  70  35.0

Что произошло:

  1. Docker создал контейнер из my-etl:v1
  2. -i (interactive) подключил stdin контейнера к stdin docker run
  3. --rm пометил контейнер «удалить после exit»
  4. Внутри запустился python etl.py, прочитал CSV из stdin, посчитал, напечатал в stdout, exit 0
  5. Контейнер удалился (--rm)

Это полный цикл: написали → собрали → запустили. С точки зрения процесса внутри контейнера — он просто читает stdin и пишет stdout, не зная что он внутри Docker.

Полный цикл: Dockerfile → image → container
DockerfileинструкцииТекстовый файл с FROM, COPY, RUN, CMD. Воспроизводимый рецепт сборки.
docker build -t my-etl:v1 .
Imagemy-etl:v1Иммутабельный артефакт. Лежит в локальном кэше, может быть push'нут в registry.
Imagemy-etl:v1Тот же образ.
docker run --rm
Containerпроцесс с overlayfsЗапущенный экземпляр. Имеет свой writable layer (модуль 6), PID/network namespace, лимиты cgroup.

Build context: что отправляется daemon’у

Когда ты пишешь docker build ., точка это build context — директория, всё содержимое которой передаётся docker daemon’у. Это важно понять, потому что size build context влияет на скорость сборки.

$ docker build -t my-etl:v1 .
 => => transferring context: 1.45kB

В нашем минимальном проекте контекст 1.45 KB. Но если в той же директории лежат:

  • .git/ директория (история коммитов, может быть 100+ MB)
  • node_modules/ (если параллельно есть JS-часть, 200+ MB)
  • __pycache__/, .venv/ (Python байткод и виртуальное окружение)
  • Локальные .parquet / .csv файлы с тестовыми данными

— весь этот мусор тоже отправится daemon’у. Каждая сборка будет занимать на 200+ MB трафика (если daemon удалённый) или I/O (если локально). Решение: .dockerignore (урок 06.05).

# .dockerignore (типичный)
.git
.venv
__pycache__
*.pyc
node_modules
.pytest_cache
.mypy_cache
data/
*.parquet
*.csv
.DS_Store

Docker не отправляет файлы, попадающие в шаблоны .dockerignore, daemon’у. Это и быстрее, и безопаснее (секреты в .env не попадут в образ случайно).

NOTE

build context может быть не только локальной директорией. docker build https://github.com/user/repo.git соберёт из git-репо. docker build - < Dockerfile собирает без контекста (можно только FROM + RUN/ENV/CMD, без COPY/ADD). Эти варианты редко нужны в DE.


Build cache: почему повторный build быстрый

При первом build pip install занял 12 секунд (скачивание pandas + numpy + установка). Запустим build второй раз:

$ docker build -t my-etl:v1 .
[+] Building 1.2s (10/10) FINISHED
 => CACHED [2/5] WORKDIR /app                                                        0.0s
 => CACHED [3/5] COPY requirements.txt .                                             0.0s
 => CACHED [4/5] RUN pip install --no-cache-dir -r requirements.txt                  0.0s
 => CACHED [5/5] COPY etl.py .                                                       0.0s

Всё CACHED, build 1.2 секунды. Почему?

Docker кэширует каждый слой по хэшу его inputs:

  • Для FROM python:3.13-slim — input это image ID базового образа. Не изменился → CACHED.
  • Для WORKDIR /app — input это инструкция и предыдущий слой. Не изменилось → CACHED.
  • Для COPY requirements.txt . — input это содержимое файла requirements.txt (его MD5 / SHA) + предыдущий слой. Файл не изменился → CACHED. RUN ниже тоже CACHED, потому что зависит от слоя с requirements.txt, который не изменился.
  • Для COPY etl.py . — если etl.py не изменился → CACHED.

Если бы мы изменили etl.py, последний слой пересобрался бы, но pip install остался бы кэшированным. Это и есть причина почему в Dockerfile сначала COPY requirements.txt + RUN pip install, потом COPY остального кода: pip install это медленная операция, мы хотим её кэшировать максимально.

# Хорошо: кэш pip install не инвалидируется при изменении etl.py
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# Плохо: любое изменение в исходном коде инвалидирует pip install
COPY . .
RUN pip install -r requirements.txt

Первый bash-скрипт: shebang, права и exit code

Полный цикл с pull / push

В реальной DE-команде workflow обычно такой:

# 1. На своей машине: разработка
git clone [email protected]:org/data-pipeline.git
cd data-pipeline
# ... редактируешь код ...

# 2. Build локально для теста
docker build -t data-pipeline:dev .
docker run --rm data-pipeline:dev   # запустить и проверить

# 3. Push в shared registry (для коллег и CI)
docker tag data-pipeline:dev ghcr.io/org/data-pipeline:v1.2.3
echo $GITHUB_PAT | docker login ghcr.io -u me --password-stdin
docker push ghcr.io/org/data-pipeline:v1.2.3

# 4. На production / staging:
docker pull ghcr.io/org/data-pipeline:v1.2.3
docker run --restart unless-stopped --name pipeline ghcr.io/org/data-pipeline:v1.2.3

# Или через compose:
# docker compose up -d

В реальной практике build + push выполняется в CI на каждый PR / merge — но это требует знаний из модуля 18 (CI и testcontainers).


Попробуй сам

Собери и запусти свой первый Dockerfile:

# Создаём минимальный проект
mkdir my-first && cd my-first

cat > hello.py <<'EOF'
import os
import sys
print(f"Hello from Docker! Python {sys.version_info.major}.{sys.version_info.minor}")
print(f"Hostname (container id): {os.uname().nodename}")
EOF

cat > Dockerfile <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY hello.py .
CMD ["python", "hello.py"]
EOF

# Build
docker build -t my-first:v1 .

# Inspect
docker images my-first
docker history my-first:v1

# Run
docker run --rm my-first:v1
# Hello from Docker! Python 3.13
# Hostname (container id): a1b2c3d4e5f6

# Запусти несколько раз -- hostname (container id) каждый раз новый
docker run --rm my-first:v1
docker run --rm my-first:v1

# Build повторно -- увидишь CACHED
docker build -t my-first:v1 .

# Измени hello.py, build снова -- только последний слой пересоберётся
echo "print('changed!')" >> hello.py
docker build -t my-first:v1 .

# Cleanup
cd .. && rm -rf my-first
docker rmi my-first:v1

Проверка знанийKnowledge check
Dockerfile содержит: FROM python:3.13-slim / COPY . . / RUN pip install -r requirements.txt / CMD ["python", "app.py"]. После build команда меняет одну строку в app.py и пересобирает. Что произойдёт с pip install кэшем?
ОтветAnswer
Кэш pip install инвалидируется и pip пересобирается полностью. Причина: COPY . . идёт перед RUN pip install -- любое изменение в любом файле проекта (включая app.py) меняет хэш input'а слоя COPY, что инвалидирует все последующие слои, включая pip install. Это плохо: pip install обычно занимает десятки секунд. Правильный порядок: сначала COPY requirements.txt ., потом RUN pip install -r requirements.txt, потом COPY . . с остальным кодом. Тогда изменение в app.py инвалидирует только финальный слой COPY, pip install остаётся CACHED.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что означает точка в команде docker build -t myapp:v1 .?

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

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

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

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