Dockerfile: как собрать образ своего приложения

DevOps Автор: Среда и версия: Docker Engine 29.7.2, Linux amd64; Python 3.14.6, Flask 3.1.2; проверено 7 сентября 2026
содержание

Dockerfile — текстовый рецепт сборки образа: какую основу взять, какие файлы скопировать и какую команду запускать. Образ хранит подготовленное окружение; контейнер запускает программу из этого образа. Ниже соберём небольшое HTTP-приложение, получим JSON через curl и проверим, какие шаги повторяются после изменения кода.

Пример рассчитан на локальное обучение. Нужен работающий Docker; команды выполняются в терминале Linux, macOS или WSL. Python на компьютере устанавливать не нужно: он будет внутри образа.

Docker / 01

Dockerfile собирает образ, docker run запускает процесс

От файлов приложения до работающего контейнераШаг 1: Dockerfile читает requirements.txt и app.py из контекста сборки. Шаг 2: docker build создаёт образ koddo-hello:1. Шаг 3: docker run запускает python app.py в контейнере, доступном через порт 8000.01 / файлы02 / docker build03 / docker runDockerfilerequirements.txtapp.pyОбразkoddo-hello:1Контейнерpython app.pyГотовые файлы и зависимостиHTTP на порту 8000
Сборка образа не запускает HTTP-сервер. Приложение начинает принимать запросы после docker run.

Подготовить приложение и Dockerfile

Создай пустую папку dockerfile-demo. В ней будут четыре файла:

dockerfile-demo/
├── app.py
├── requirements.txt
├── Dockerfile
└── .dockerignore

В app.py сохрани приложение с одним адресом /:

from flask import Flask

app = Flask(__name__)


@app.get("/")
def hello():
    return {"message": "Hello, Koddo!"}


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

Содержимое requirements.txt:

Flask==3.1.2

Создай Dockerfile без расширения:

FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
USER 10001
EXPOSE 8000
CMD ["python", "app.py"]

Инструкции читаются сверху вниз:

ИнструкцияЧто происходит в примере
FROMБерёт базовый образ с Python
WORKDIRДелает /app рабочей папкой следующих команд
COPYКопирует файл с компьютера в образ
RUNУстанавливает Flask во время сборки
USERЗадаёт числовой UID 10001 для запуска приложения без root
EXPOSEОписывает порт приложения; сам порт не публикует
CMDЗадаёт команду по умолчанию для запуска контейнера

Различия между инструкциями закреплены в справочнике Dockerfile. Сервер слушает 0.0.0.0 внутри контейнера, чтобы принимать запросы через его сетевой интерфейс. Доступ с компьютера настроим отдельно через -p.

Контекст сборки и .dockerignore

В .dockerignore запиши:

.git
.venv
__pycache__
*.pyc
.env
.env.*

Контекст — набор файлов, доступных сборщику. В команде docker build ... . последняя точка означает текущую папку. COPY app.py . берёт исходник относительно этого контекста, а помещает в /app, заданную через WORKDIR.

.dockerignore исключает лишние файлы из контекста. Это особенно полезно, если позже появится COPY . .: локальное окружение и файлы .env не попадут в образ. Пароль, уже записанный в app.py, исключение .env не защитит. Правила контекста и исключений описаны в документации Docker.

Собрать образ и запустить контейнер

Перейди в dockerfile-demo и выполни:

docker build -t koddo-hello:1 .
docker run -d --name koddo-hello -p 127.0.0.1:8000:8000 koddo-hello:1
curl --fail http://127.0.0.1:8000/

Когда сервер запустится, ответ будет таким:

{"message":"Hello, Koddo!"}

-t назначает образу имя и тег. -d запускает контейнер в фоне, --name задаёт его имя. Адрес 127.0.0.1:8000:8000 публикует порт 8000 контейнера на порту 8000 только локального интерфейса компьютера. Если этот порт занят, замени левый номер на 8001 и обращайся к http://127.0.0.1:8001/.

Если запрос пришёл раньше запуска сервера, посмотри логи и повтори curl:

docker logs koddo-hello

Flask сообщит, что используется development server. Для учебного запуска это ожидаемо; для сайта с пользователями нужен сервер WSGI, например Gunicorn или Waitress. Контейнеризация сама по себе не снимает это ограничение Flask.

RUN, CMD и ENTRYPOINT: когда выполняется команда

RUN выполняется при сборке и меняет содержимое образа. Если написать RUN python app.py, сборка застрянет на работающем сервере. CMD сохраняет команду для будущего запуска; сборщик её не выполняет.

Команду CMD можно заменить аргументами после имени образа:

docker run --rm koddo-hello:1 python --version

Этот контейнер напечатает версию Python и завершится, не запуская app.py. Флаг --rm удалит его после выполнения команды.

Если образ предназначен для одной программы, можно заменить последнюю строку Dockerfile на две:

ENTRYPOINT ["python"]
CMD ["app.py"]

Тогда docker run IMAGE --version запустит python --version: аргументы заменят CMD, а ENTRYPOINT останется. Это альтернативная конфигурация; для основного примера оставь исходный CMD. Для принудительной замены ENTRYPOINT существует флаг --entrypoint.

Форма со списком в двойных кавычках запускает программу напрямую, без дополнительной оболочки. Правила совместной работы CMD и ENTRYPOINT приведены в документации Docker.

Что пересобирается после изменения кода

Замени Hello, Koddo! в app.py на Hello again!, затем собери образ с новым тегом:

docker build --progress=plain -t koddo-hello:2 .

В проверочной сборке шаг RUN pip install --no-cache-dir -r requirements.txt получил отметку CACHED, а COPY app.py . выполнился заново. Причина в порядке инструкций: файл зависимостей скопирован и обработан до исходника приложения. Если сначала выполнить COPY . ., изменение app.py затронет и следующую установку зависимостей.

Теперь добавь в конец requirements.txt комментарий # cache check и повтори сборку. Даже без смены версии Flask содержимое файла изменилось: шаг копирования и следующий RUN pip install выполнятся заново. Правила проверки файлов и повторного использования слоёв описаны в документации кеша Docker.

--no-cache-dir отключает файловый кеш pip, а не кеш слоёв Docker. Для полной пересборки шагов Docker есть docker build --no-cache; скачивание новой версии базового образа отдельно запрашивается флагом --pull.

Работающий контейнер продолжает использовать старый образ. Чтобы увидеть новый ответ, пересоздай его:

docker stop koddo-hello
docker rm koddo-hello
docker run -d --name koddo-hello -p 127.0.0.1:8000:8000 koddo-hello:2
curl --fail http://127.0.0.1:8000/

После запуска ответ изменится на {"message":"Hello again!"}. В этом примере нет bind mount: редактирование файла на компьютере не меняет уже созданный контейнер.

Почему один тег не гарантирует одинаковый образ

python:3.14-slim может со временем указывать на обновлённый образ. При проверке использовался Python 3.14.6. Точная версия Flask закреплена, но его транзитивные зависимости в этом учебном requirements.txt не зафиксированы.

Для воспроизводимой сборки приложения закрепляют базовый образ по digest и полное дерево зависимостей с контрольными суммами. Обновления затем делают явно, чтобы получать исправления безопасности. Docker разбирает компромисс между обновлением образов и закреплением digest в рекомендациях по сборке.

По окончании упражнения останови и удали контейнер:

docker stop koddo-hello
docker rm koddo-hello

Дальше добавь к приложению PostgreSQL по руководству Docker Compose. Для автоматической сборки образа после изменения кода пригодится разбор CI/CD.

Источники