Введение: Зачем Python-разработчику нужен Docker?

Представьте ситуацию: вы написали шикарного Telegram-бота, отладили его на своем ноутбуке с Python 3.11 и последней версией любимой библиотеки. Вы отправляете код коллеге, а у него падает с ошибкой, потому что у него Python 3.9 и какая-то зависимость встала криво. Или, что еще хуже, вы пытаетесь выкатить это на сервер, а там системный администратор смотрит на ваш requirements.txt и тяжело вздыхает, потому что для установки одной из библиотек нужна системная утилита, которой нет и не будет. Знакомо?

Так вот, Docker — это инструмент, который эту проблему решает раз и навсегда.

Если говорить просто, Docker позволяет упаковать ваше приложение со всем его окружением — конкретной версией Python, всеми библиотеками из requirements.txt, нужными системными пакетами и вашим кодом — в один изолированный «ящик», который называется контейнером.

Этот «ящик» можно запустить где угодно: на вашем Windows-ноутбуке, на MacBook коллеги, на Linux-сервере в облаке. И он будет работать абсолютно одинаково. Проблема «у меня работает, а у тебя нет» просто исчезает.

Коротко, что это дает лично вам, как Python-разработчику:

  1. Собрал один раз — запустил везде. Вы создаете образ своего приложения, и этот образ можно передать кому угодно или загрузить на сервер. Больше не нужно настраивать окружение вручную и молиться, чтобы всё завелось.

  2. Никаких больше конфликтов зависимостей. У вас может быть один проект на старой версии Django, а другой — на FastAPI с последними библиотеками. Они будут жить в своих изолированных контейнерах и никогда не узнают друг о друге, не засоряя вашу основную систему. Забудьте про головную боль с virtualenv.

  3. Окружение, идентичное продакшену. Ваш контейнер для разработки будет точной копией того, что будет крутиться на боевом сервере. Это убивает целый класс багов, которые проявляются только после деплоя.

В этой статье мы не будем погружаться в занудную теорию. Мы сделаем то, что любим больше всего — напишем код. Мы возьмем простейшее веб-приложение на Flask, шаг за шагом напишем для него «инструкцию по сборке» (тот самый Dockerfile), упакуем его в контейнер и запустим. Поехали

2. Подготовка к работе: Что нам понадобится?

Теория — это, конечно, хорошо, но давайте переходить к практике. Чтобы построить наш первый контейнер, нам нужно всего две вещи: сам Docker и небольшое Python-приложение, которое мы и будем "упаковывать".

Шаг 1: Устанавливаем Docker

Первым делом нужно установить Docker Desktop. Это приложение, которое включает в себя всё необходимое для работы с контейнерами на вашем локальном компьютере. Процесс установки довольно простой, просто скачайте версию для вашей операционной системы и следуйте инструкциям.

Шаг 2: Создаем наше подопытное Python-приложение

Мы не будем усложнять. Наш "подопытный кролик" — это простое веб-приложение на Flask, которое будет отдавать одну-единственную строчку в браузере.

Создайте на своем компьютере папку с названием my-python-app. Внутри этой папки создайте два файла: app.py и requirements.txt.

Вот так должна выглядеть структура вашего проекта:

/my-python-app
|
|-- app.py
|-- requirements.txt

Содержимое app.py:

Скопируйте этот код в файл app.py. Это минимальное Flask-приложение.

from flask import Flask

# Создаем экземпляр приложения Flask
app = Flask(__name__)

# Определяем маршрут для главной страницы
@app.route('/')
def hello_world():
    # Эта строка — то, что мы увидим в браузере
    return 'Привет, Habr, из Docker-контейнера!'

# Эта часть нужна, чтобы запустить сервер, когда мы запускаем файл напрямую
if __name__ == '__main__':
    # Важный момент: host='0.0.0.0' делает сервер видимым
    # за пределами контейнера.
    app.run(host='0.0.0.0', port=5000)

Важное замечание: host='0.0.0.0' — это не случайность. Этот адрес говорит нашему приложению "слушать" запросы со всех сетевых интерфейсов. Если оставить стандартный 127.0.0.1 (localhost), то извне контейнера достучаться до нашего приложения будет невозможно. Запомните это правило: внутри Docker почти всегда используется 0.0.0.0.

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

Теперь определим зависимости. Впишите эти две строчки в файл requirements.txt.

Flask==3.0.0
gunicorn==22.0.0

Зачем здесь gunicorn? Встроенный сервер Flask отлично подходит для разработки и отладки, но он не предназначен для "боевых" условий. Gunicorn — это полноценный, production-ready WSGI-сервер. Мы сразу будем делать "правильно" и запускать наше приложение внутри контейнера с его помощью.

Отлично, подготовка завершена! У нас есть работающий Docker и простое приложение. Теперь самое интересное — мы напишем инструкцию, по которой Docker соберет для этого приложения его персональный "ящик".

3. Сердце контейнеризации: Создание Dockerfile

Итак, у нас есть "стройматериалы" (наше Flask-приложение) и "завод" (установленный Docker). Теперь нам нужен "чертеж", по которому завод будет собирать наш продукт. В мире Docker этот чертеж называется Dockerfile.

Что такое Dockerfile?

Это простой текстовый файл. В нем мы по шагам, команда за командой, описываем, как собрать образ нашего приложения. Думайте об этом как о рецепте: возьмите то, добавьте это, запустите вот так.

Прямо в корне нашей папки my-python-app, рядом с app.py и requirements.txt, создайте файл с именем Dockerfile.

Важно: У файла не должно быть расширения. Просто Dockerfile.

А теперь давайте напишем наш "рецепт" шаг за шагом. Откройте этот файл и начнем добавлять в него строки.


Шаг 1: Выбираем фундамент

# Используем официальный образ Python как основу
FROM python:3.11-slim

Каждый образ Docker строится на основе другого, "родительского" образа. Команда FROM — это всегда первая строчка в Dockerfile. Здесь мы говорим: "Взять за основу официальный образ с уже установленным Python версии 3.11".

Почему slim? Это "облегченная" версия образа. В ней нет лишних пакетов и документации, которые нам не нужны для запуска приложения. В результате наш итоговый образ будет весить значительно меньше. Золотое правило: всегда старайтесь делать образы как можно меньше.

Шаг 2: Указываем рабочую папку

# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app

Команда WORKDIR делает две вещи: создает директорию /app внутри нашего будущего контейнера (если ее нет) и делает ее текущей. Это значит, что все последующие команды (COPY, RUN и т.д.) будут выполняться из этой папки. Это хорошая практика, чтобы не разбрасывать файлы нашего приложения по всей файловой системе контейнера.

Шаг 3: Устанавливаем зависимости (с хитростью)

# Копируем файл с зависимостями и устанавливаем их
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

А вот и первый хитрый момент, который отличает хороший Dockerfile от плохого. Казалось бы, почему не скопировать сразу все файлы проекта?

Дело в том, как Docker кэширует слои. Каждый RUN, COPY, ADD создает новый слой. Когда вы пересобираете образ, Docker проверяет, изменилась ли команда или файлы, которые она использует. Если нет — он берет готовый слой из кэша.

Зависимости в requirements.txt меняются редко, а вот наш код в app.py — постоянно. Разделяя эти шаги, мы добиваемся того, что Docker будет заново устанавливать все библиотеки (долгий процесс) только в том случае, если мы изменим файл requirements.txt. Если же мы поменяем только код в app.py, Docker возьмет уже готовый слой с установленными библиотеками из кэша, и сборка пройдет за секунды.

--no-cache-dir — полезный флаг, который говорит pip не сохранять кэш скачанных пакетов. Это еще один способ уменьшить итоговый размер образа.

Шаг 4: Копируем код нашего приложения

# Копируем весь остальной код приложения
COPY . .

Теперь, когда зависимости установлены, мы копируем все остальные файлы из текущей директории (первая точка) в рабочую директорию /app внутри контейнера (вторая точка).

Шаг 5: Открываем порт

# "Сообщаем" Docker, что наше приложение будет работать на порту 5000
EXPOSE 5000

Команда EXPOSE — это, по сути, документация. Она не открывает порт по-настоящему, но сообщает Docker (и человеку, который будет этот образ использовать), что приложение внутри контейнера ожидает подключения на порту 5000.

Шаг 6: Указываем команду для запуска

# Указываем команду, которая запустится при старте контейнера
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Это финальный аккорд. Команда CMD определяет, что будет выполнено при запуске контейнера. Мы запускаем наш gunicorn, приказываем ему слушать на всех интерфейсах (0.0.0.0) на порту 5000 и указываем, что запускать нужно объект app из модуля app (app.py).


Итоговый рецепт

Вот как должен выглядеть ваш Dockerfile целиком:

# 1. Используем официальный образ Python как основу
FROM python:3.11-slim

# 2. Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app

# 3. Копируем файл с зависимостями и устанавливаем их (используем кэш)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 4. Копируем весь остальной код приложения
COPY . .

# 5. "Сообщаем" Docker, что наше приложение будет работать на порту 5000
EXPOSE 5000

# 6. Указываем команду, которая запустится при старте контейнера
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

4. Сборка и запуск: Оживляем наш контейнер

Все подготовительные работы завершены. Сейчас мы выполним всего две команды в терминале, чтобы собрать и запустить наше приложение.

Убедитесь, что вы находитесь в терминале (командной строке) внутри вашей папки my-python-app, там же, где лежат все наши файлы.

Шаг 1: Собираем образ с помощью docker build

Эта команда читает наш Dockerfile и выполняет все инструкции по очереди, создавая в итоге готовый образ.

Выполните в терминале:

docker build -t my-python-app .

Давайте разберем, что здесь происходит:

  • docker build: Это основная команда для сборки образа.

  • -t my-python-app: Флаг -t (от "tag") позволяет нам присвоить образу имя и, опционально, тег. Мы назвали наш образ my-python-app. Это гораздо удобнее, чем работать со случайным ID, который Docker присвоил бы ему по умолчанию.

  • . (точка в конце): Это очень важная часть. Точка указывает на контекст сборки. Она говорит Docker: "Ищи Dockerfile в текущей директории (.) и отсюда же бери все файлы для копирования в образ (например, app.py и requirements.txt)".

После запуска команды вы увидите, как Docker поочередно выполняет каждый шаг из нашего Dockerfile: FROM, WORKDIR, COPY, RUN... Если все прошло успешно, в конце вы увидите сообщение о том, что образ собран.

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

docker images

Вы должны увидеть в списке свежесозданный образ my-python-app.

Шаг 2: Запускаем контейнер из образа с помощью docker run

Образ — это, по сути, шаблон, выключенный "слепок" нашего приложения. Чтобы он ожил, из него нужно запустить контейнер. Контейнер — это работающий экземпляр образа.

Выполните эту команду:

docker run -d -p 8080:5000 --name my-running-app my-python-app

Эта команда выглядит сложнее, но тут все логично:

  • docker run: Основная команда для запуска контейнера.

  • -d: От "detached". Этот флаг запускает контейнер в фоновом режиме. Ваш терминал сразу освободится, а контейнер продолжит работать "за сценой".

  • -p 8080:5000: Флаг -p (от "publish") — ключевой момент. Он "пробрасывает" порт изнутри контейнера наружу, на нашу машину. Мы связываем порт 8080 на нашем компьютере (хост-машине) с портом 5000 внутри контейнера. Именно на 5000-м порту слушает наше приложение (gunicorn).

  • --name my-running-app: Мы даем нашему работающему контейнеру человеческое имя my-running-app. Это поможет нам в дальнейшем им управлять (останавливать, смотреть логи и т.д.).

  • my-python-app: В конце мы указываем имя образа, из которого мы хотим создать контейнер.

Шаг 3: Проверяем результат!

Самый приятный момент. Откройте ваш любимый браузер и перейдите по адресу:

http://localhost:8080

Если вы все сделали правильно, вы увидите заветную надпись:

Привет, Habr, из Docker-контейнера!

Поздравляю! Вы только что упаковали Python-приложение в контейнер и запустили его. Ваш код теперь работает в полностью изолированной среде, и вы можете быть уверены, что он точно так же запустится на любой другой машине, где есть Docker.

5. Управление контейнерами: Основные команды

Наше приложение работает в фоновом режиме, но оно не превратилось в неуправляемый черный ящик. У нас есть полный контроль над его жизненным циклом. Вот базовый "джентльменский набор" команд, который вы будете использовать постоянно.

1. Посмотреть, что запущено: docker ps

Эта команда — ваш главный инструмент мониторинга. Она показывает список всех активных контейнеров.

Выполните в терминале:

docker ps

Вы увидите аккуратную табличку с информацией о вашем контейнере: его ID, из какого образа он создан (my-python-app), статус (Up for...), информацию о портах (0.0.0.0:8080->5000/tcp) и, конечно же, его имя (my-running-app).

  • Лайфхак: Если хотите увидеть вообще все контейнеры, включая те, что были остановлены, добавьте флаг -a: docker ps -a.

2. Заглянуть внутрь: docker logs

Что, если в приложении произошла ошибка? Или вы просто хотите посмотреть логи gunicorn, чтобы увидеть входящие запросы? Для этого есть команда docker logs.

docker logs my-running-app

Она выведет в терминал все, что ваше приложение напечатало с момента запуска. Это невероятно полезно для отладки.

  • Лайфхак: Чтобы следить за логами в реальном времени (как команда tail -f в Linux), добавьте флаг -f: docker logs -f my-running-app. Нажмите Ctrl+C, чтобы выйти из этого режима.

3. Остановить контейнер: docker stop

Когда вам нужно выключить ваше приложение, используйте команду docker stop. Она аккуратно посылает контейнеру сигнал о завершении работы, давая ему время корректно остановиться.

docker stop my-running-app

Терминал вернет вам имя остановленного контейнера. Если вы теперь снова выполните docker ps, список будет пуст. А вот docker ps -a покажет ваш контейнер со статусом Exited.

4. Запустить остановленный контейнер: docker start

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

docker start my-running-app

Он запустится с теми же параметрами, с которыми был создан изначально (с тем же пробросом портов и т.д.).

5. Удалить контейнер: docker rm

Когда контейнер вам больше не нужен и вы просто хотите освободить место, его можно удалить.

Важно: Удалить можно только остановленный контейнер.

# Сначала останавливаем, если он еще работает
docker stop my-running-app

# Затем удаляем
docker rm my-running-app

После этого контейнер исчезнет навсегда. Обратите внимание: удаляется только контейнер, а не образ my-python-app. Вы в любой момент можете создать новый контейнер из этого образа с помощью команды docker run.


Вот и все! Этих пяти команд — ps, logs, stop, start, rm — вам хватит для 90% повседневных задач по управлению контейнерами. Теперь вы не просто создали контейнер, но и умеете им управлять.

6. Лучшие практики и что дальше?

Поздравляю! Вы прошли путь от простого Python-скрипта до работающего, изолированного контейнера. Это уже огромный шаг. Но, как и в любом ремесле, здесь есть свои хитрости, которые отличают новичка от профессионала.

Немного о лучших практиках

Вот пара простых советов, которые сразу сделают ваши Docker-образы лучше, меньше и безопаснее.

1. Используйте .dockerignore

Когда вы выполняете команду docker build ., Docker сначала запаковывает весь "контекст" (все файлы и папки в текущей директории) и отправляет его своему движку. В этот архив попадает всё: виртуальное окружение .venv, кэш Python __pycache__, папки .git и другие служебные файлы, которые вашему приложению в контейнере абсолютно не нужны.

Чтобы этого избежать, создайте в корне проекта файл .dockerignore (по аналогии с .gitignore). Все, что вы в нем перечислите, будет проигнорировано при сборке.

Пример хорошего .dockerignore для Python-проекта:

# Исключаем виртуальное окружение
.venv
venv
env

# Исключаем кэш Python
__pycache__/
*.pyc
*.pyo

# Исключаем служебные файлы IDE и ОС
.idea/
.vscode/
.DS_Store

# Исключаем Git
.git
.gitignore

Результат: Контекст сборки становится меньше (сборка быстрее), а итоговый образ не содержит мусора.

2. Не работайте под root

По умолчанию все процессы внутри контейнера запускаются от имени суперпользователя (root). С точки зрения безопасности — это плохая идея. Если злоумышленник найдет уязвимость в вашем приложении, он получит права root внутри контейнера, что может привести к серьезным последствиям.

Правильный подход — создать отдельного, непривилегированного пользователя и запускать приложение от его имени.

Добавьте эти строчки в ваш Dockerfile перед командой CMD:

# ... после COPY . .

# Создаем пользователя appuser
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Переключаемся на этого пользователя
USER appuser

# Указываем команду для запуска (она выполнится от имени appuser)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Это простой шаг, который значительно повышает безопасность вашего контейнера.


Что дальше? Ваш путь в мире Docker

Docker — это целая вселенная. Вы сделали первый и самый важный шаг, но впереди еще много интересного. Вот что я настоятельно рекомендую изучить дальше:

Docker Compose: дирижер для вашего оркестра

Наше приложение пока одиноко. Но что, если ему понадобится база данных (например, PostgreSQL) или кэш (Redis)? Запускать каждый контейнер отдельной docker run командой с кучей параметров — путь в никуда.

Здесь на сцену выходит Docker Compose. Это инструмент, который позволяет в простом YAML-файле описать всю связку сервисов вашего приложения: веб-сервер, базу данных, фоновые обработчики и т.д. Одной командой docker-compose up вы поднимаете все это хозяйство, и сервисы могут общаться друг с другом по сети, которую Docker Compose создает автоматически.

Это логичный следующий шаг в вашем обучении. Умение работать с Docker Compose — это уже стандарт де-факто для локальной разработки в большинстве компаний.

Container Registry: ваш личный склад образов

Сейчас ваши образы хранятся только на вашем компьютере. Чтобы использовать их на сервере или поделиться с коллегой, их нужно куда-то загрузить. Для этого существуют реестры контейнеров. Самый известный — Docker Hub. Вы можете бесплатно хранить там свои публичные образы (и несколько приватных) и скачивать их где угодно с помощью команды docker pull.

Домашнее задание

Теория без практики мертва. Чтобы по-настоящему усвоить материал, попробуйте выполнить эти три задания. Они помогут вам закрепить ключевые навыки и столкнуться с реальными задачами, которые возникают в работе с Docker.

Задание 1: Косметический ремонт (простое)

Цель: Убедиться, что вы поняли базовый цикл разработки: «изменил код -> пересобрал образ -> запустил новый контейнер».

Что нужно сделать:

  1. Откройте файл app.py.

  2. Измените строку, которую возвращает функция hello_world(). Например, на "Docker - это мощь! Мой первый контейнер работает.".

  3. Пересоберите Docker-образ. Дайте ему новый тег, чтобы не затереть старый. Например: docker build -t my-python-app:v2 .

  4. Остановите и удалите старый контейнер (my-running-app), если он еще запущен.

  5. Запустите новый контейнер из образа my-python-app:v2. Не забудьте пробросить порты.

  6. Проверьте результат в браузере по адресу http://localhost:8080. Вы должны увидеть новое сообщение.

Что это проверяет: Ваше умение итерировать разработку, обновлять образы и управлять жизненным циклом контейнеров.

Задание 2: Новая зависимость (среднее)

Цель: Научиться добавлять новые библиотеки в проект и передавать в контейнер переменные окружения.

Что нужно сделать:

  1. Добавьте в requirements.txt новую библиотеку: requests.

  2. Измените app.py так, чтобы он при каждом запросе обращался к какому-нибудь публичному API. Например, к API для получения фактов о кошках. Полученный факт нужно возвращать в браузер.

    • Подсказка: вам понадобится import requests и requests.get('https://catfact.ninja/fact').json()['fact'].

  3. Пересоберите образ. Обратите внимание, как Docker на этот раз не использует кэш для шага RUN pip install..., потому что файл requirements.txt изменился.

  4. Запустите контейнер из нового образа. Убедитесь, что при каждом обновлении страницы в браузере вы видите новый случайный факт.

Что это проверяет: Ваше понимание работы с зависимостями и кэшированием слоев в Docker.

Задание 3: Работа над ошибками (продвинутое)

Цель: Применить на практике лучшие практики по оптимизации и безопасности, описанные в статье.

Что нужно сделать:

  1. Создайте файл .dockerignore в корне проекта. Добавьте в него как минимум директории __pycache__ и .venv (даже если у вас ее нет, это хорошая привычка).

  2. Измените Dockerfile, чтобы приложение запускалось от имени непривилегированного пользователя.

    • Добавьте команды RUN addgroup ... && adduser ... для создания пользователя.

    • Добавьте команду USER, чтобы переключиться на этого пользователя.

  3. Пересоберите образ с теми же улучшениями (факты о кошках).

  4. Запустите контейнер. Внешне ничего не должно измениться — приложение должно работать так же. Но теперь ваш контейнер стал безопаснее и собирается немного эффективнее.

Что это проверяет: Ваше умение писать не просто работающие, а качественные и безопасные Dockerfile.

Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.

Уверен, у вас все получится. Вперед, к практике!

Комментарии (5)


  1. mayorovp
    05.11.2025 08:09

    --no-cache-dir — полезный флаг, который говорит pip не сохранять кэш скачанных пакетов. Это еще один способ уменьшить итоговый размер образа.

    В современных докерфайлах вместо этого стоило бы подмонтировать /root/.cache/pip как слой кэша:

    RUN --mount=type=cache,target=/root/.cache/pip \
        pip install -r requirements.txt
    

    Кроме того, имеет смысл рассмотреть использование декларативного mopy вместо докерфайла.


    1. amarkevich
      05.11.2025 08:09

      для локальной разработки достаточно и кэширования слоёв; да и путь выглядит так, что под виндой ожидаем проблемы. в случае клауд билда - кэш всё равно пустой.


      1. mayorovp
        05.11.2025 08:09

        О каких проблемах под виндой речь? Путь-то указан строго внутри контейнера, а вопросы хранения снаружи полностью во власти BuildKit.


  1. Evgeny_173
    05.11.2025 08:09

    Отличная вводная статья, спасибо!


  1. ktibr0
    05.11.2025 08:09

    Хорошая статья для новичка, жаль, не попалась раньше, когда начинал ознакомление с докером.

    Единственное, свежие версии docker compose запускаются командой docker compose , а не docker-compose