
Что Docker даёт Python-проекту
Docker упаковывает приложение вместе с интерпретатором, библиотеками и системными зависимостями в образ. Запуск образа на другой машине даёт ту же среду, что и у вас, если совпадает архитектура процессора. Для Python это снимает главную боль — расхождение версий пакетов и ситуацию «локально работает, на сервере нет».
Читайте также: Docker: как удалить контейнер — все способы и команды, Docker остановить контейнер: stop, kill и graceful shutdown.
Второй эффект — фиксация версии интерпретатора. Указав в Dockerfile python:3.12-slim, вы получаете 3.12 независимо от того, какой Python установлен на сервере. virtualenv так не умеет: он изолирует пакеты, но берёт интерпретатор с хоста.
Ограничения тоже реальны. Контейнер добавляет слой абстракции: сеть, логи, права пользователя, тома настраиваются отдельно. Для скрипта, который запускается раз в неделю локально, это лишняя работа — хватит venv. Не подходит Docker и там, где нужен прямой доступ к GPU или специфичному оборудованию, хотя nvidia-container-toolkit закрывает часть таких задач. На некоторых shared-хостингах контейнеры вообще запрещены.
Dockerfile для Python-приложения
Минимальный рабочий вариант для веб-сервиса выглядит так:
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Разберём основные строки. PYTHONUNBUFFERED=1 отключает буферизацию stdout, иначе логи появляются в docker logs с задержкой или теряются при падении процесса. PYTHONDONTWRITEBYTECODE=1 не даёт создавать .pyc внутри образа. WORKDIR задаёт рабочую директорию, а копирование requirements.txt до остального кода — приём для кэша, о нём ниже.
Запуск через CMD в виде JSON-массива выполняется без shell: процесс получает SIGTERM напрямую, и docker stop завершает приложение корректно, а не убивает его по таймауту. Для скриптов вместо сервера подойдёт CMD ["python", "main.py"].
Какой базовый образ выбрать
| Образ | Размер | Особенности |
|---|---|---|
| python:3.12 | порядка гигабайта | полный Debian с компиляторами, сборка проходит почти всегда |
| python:3.12-slim | в несколько раз меньше | готовые колёса PyPI работают, компилятор придётся доустанавливать |
| python:3.12-alpine | самый компактный | musl вместо glibc: manylinux-колёса не подходят |
Slim — разумный выбор по умолчанию: размер заметно меньше полного образа, а совместимость с бинарными колёсами сохраняется. Alpine выигрывает в размере, но для numpy, cryptography или psycopg2 pip начнёт сборку из исходников, и в образ придётся добавить build-base, musl-dev и заголовочные файлы. Часто это съедает всю экономию. Полный образ оправдан, когда нужны системные библиотеки «из коробки» и разбираться с их установкой не хочется.
Цифры у разных версий отличаются, поэтому проверяйте свой тег командой docker images после сборки.
Сборка, запуск и кэширование слоёв
Собрать и запустить образ:
docker build -t myapp .
docker run --rm -p 8000:8000 --env-file .env myapp
Флаг -p 8000:8000 публикует порт контейнера на хост — без него приложение доступно только изнутри сети Docker. --rm удаляет контейнер после остановки, --env-file подставляет переменные окружения из файла. Секреты внутрь образа не копируйте: они останутся в слое и будут видны через docker history.
Порядок команд влияет на скорость пересборки. Docker кэширует каждый слой и переиспользует его, пока не изменилось содержимое. Если сначала скопировать весь код (COPY . .), а затем ставить зависимости, то любая правка в одном .py-файле обнулит кэш и запустит pip install заново. Поэтому requirements.txt копируется отдельно и раньше.
Рядом с Dockerfile нужен .dockerignore. Без него в образ попадут .venv, __pycache__, .git и .env — минимум это лишние десятки мегабайт и медленный COPY, максимум утечка секретов:
.venv
__pycache__
*.pyc
.git
.env
Compose и режим разработки
Когда приложению нужна база или очередь, окружение удобнее описать в compose-файле:
services:
app:
build: .
ports:
- "8000:8000"
env_file: .env
volumes:
- ./app:/code/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
Запуск — docker compose up. Внутри сети приложение обращается к базе по имени сервиса, то есть по хосту db, а не localhost. Это частая ошибка при переносе кода: локально драйвер подключается к 127.0.0.1, а в контейнере такой адрес указывает на сам контейнер. Параметры подключения выносите в переменные окружения, а не в код.
Volume ./app:/code/app подменяет код в контейнере файлами с хоста — правки видны без пересборки. Для продакшена этот приём не годится: образ перестаёт соответствовать тому, что реально выполняется. Там код должен лежать внутри образа, а состояние (данные БД, загруженные файлы) — в отдельных томах. Для одноразовых задач вместо compose достаточно docker run --rm myapp python script.py.
Частые вопросы
Нужен ли virtualenv внутри контейнера? Нет. Контейнер уже изолирует зависимости, а pip ставит пакеты в системный site-packages образа; виртуальное окружение добавит лишний слой и усложнит пути. Исключение — multi-stage сборка: пакеты ставят в отдельный префикс командой pip install --prefix=/install -r requirements.txt, а во втором этапе подключают через COPY --from=builder /install /usr/local. Так компиляторы не попадают в финальный образ.
Чем заменить Docker, если он не подходит? Для локальной разработки — venv или conda: они легче и не требуют демона. Для воспроизводимости без контейнеров — lock-файлы с точными версиями (requirements.txt, poetry.lock, uv.lock) плюс фиксация версии Python. Для изоляции с более жёсткими границами — виртуальные машины или systemd-юниты под отдельным пользователем.
Как уменьшить образ? Возьмите slim-базовый образ, ставьте пакеты с --no-cache-dir, добавьте .dockerignore, объединяйте связанные RUN в один слой и используйте multi-stage сборку. Каждый установленный системный пакет становится слоем, поэтому чистить кэш apt нужно в той же команде RUN, где шла установка.
Почему приложение не видно с хоста? Проверьте три вещи: сервер слушает 0.0.0.0, а не 127.0.0.1; порт опубликован через -p; в команде запуска нет опечатки в номере порта. Внутри контейнера localhost — это сам контейнер, поэтому bind на 127.0.0.1 делает сервис недоступным снаружи.
Начните с одного сервиса: соберите Dockerfile на slim-образе, добавьте .dockerignore и запустите приложение через docker run с публикацией порта. Когда появятся база данных и другие зависимости, перенесите описание в compose. Держите версии Python и пакетов зафиксированными в файлах — тогда образ будет собираться и вести себя одинаково на любой машине.