TL;DR
Это Docker-шаблон для Python + Poetry, рассчитанный на реальную работу, а не учебные примеры: воспроизводимое окружение, удобный dev-workflow, отдельные сборки под прод, dev, Jupyter и AI-инструменты.
Автор использует его в основном для DS/ML-задач, где важнее скорость и предсказуемость, чем экономия пары мегабайт образа. Шаблон обкатан в бою, экономит время и легко кастомизируется под свои нужды.
Почти каждый Python-проект начинается одинаково: выбрать версию Python, настроить зависимости, виртуальное окружение, переменные среды, команды запуска. На практике самые болезненные места здесь — управление зависимостями и воспроизводимость окружения: разные версии библиотек, несовпадающие Python, локальные костыли, которые сложно повторить на другой машине или сервере.
Docker помогает изолировать окружение, но сам по себе он не решает Python-специфичные задачи. Его нужно правильно наполнить: учесть работу Poetry, кеширование зависимостей, структуру проекта и базовые практики, которые одинаково хорошо работают и в разработке, и в продакшене. Именно такой шаблон мы и будем собирать дальше.
В этой статье мы шаг за шагом соберём базовый Docker-шаблон для Python с Poetry, который удобно использовать и для разработки, и для прода. В основе будет минимальное и воспроизводимое окружение, а всё остальное - Vim как IDE, Jupyter, AI-инструменты вроде Codex или Gemini - вынесено в отдельные образы и слои, которые можно подключать по мере необходимости. Начнём с самого главного - разберём Dockerfile и поймём, как собрать прочную и расширяемую базу для Python-проекта.
Собираем базовый Dockerfile для Python и Poetry
Для этого шаблона мы будем использовать multistage-сборку Docker-образов. Это позволяет разделить один Dockerfile на логические слои: базовое Python-окружение, сборку зависимостей, production-образ и отдельные dev- и IDE-окружения. Каждый слой решает свою задачу и может использоваться независимо.
Такой подход даёт сразу несколько преимуществ: мы не тащим dev-инструменты в продакшен, переиспользуем общий базовый образ, ускоряем сборку за счёт кеширования и можем легко добавлять новые сценарии — например, Vim-IDE, Jupyter или AI-инструменты — не усложняя основную сборку. В результате один Dockerfile покрывает и разработку, и продакшен, оставаясь при этом читаемым и расширяемым.
В основе всех последующих образов лежит слой python-base. Это базовый образ, в котором мы один раз настраиваем всё, что относится к Python-окружению: систему, пользователя, Python нужной версии, виртуальное окружение и Poetry. Все остальные build-таргеты наследуются от него и не дублируют эту логику.
В этом блоке определим базовый образ python-base и набор аргументов сборки, управляющих конфигурацией контейнера: пользователем, системными зависимостями, версиями Python и Poetry, а также региональными настройками. Использование ARG позволяет параметризовать сборку и переиспользовать образ в разных сценариях без изменения кода Dockerfile:
FROM archlinux:base-devel AS python-base ARG TZ=Asia/Vladivostok ARG DOCKER_HOST_UID=1000 ARG DOCKER_HOST_GID=1000 ARG DOCKER_USER=devuser ARG DOCKER_USER_HOME=/home/devuser ARG MIRROR_LIST_COUNTRY=RU ARG BUILD_PACKAGES="pyenv git gnupg sudo postgresql-libs mariadb-libs openmp" ARG PYTHON_VERSION=3.14 ARG POETRY_VERSION=2.2.1
В качестве базового образа здесь используется archlinux:base-devel :harold: Такой выбор может показаться спорным: Arch это rolling-release дистрибутив, а значит, версии пакетов постоянно обновляются, что не всегда ассоциируется со стабильностью и предсказуемостью, особенно в прод окружениях. Кроме того, Arch реже встречается в Docker-примерах по сравнению с Debian или Ubuntu.
Тем не менее, для задач, которые решает этот шаблон, такой выбор вполне оправдан. Arch даёт доступ к актуальным версиям библиотек, языков и системных компонентов без подключения сторонних репозиториев, а образ base-devel сразу включает полный набор инструментов для сборки нативных зависимостей. Это упрощает установку Python-пакетов с C-расширениями и хорошо масштабируется: в следующих слоях мы будем добавлять Node.js, AI-инструменты и другие dev-утилиты, и Arch позволяет делать это напрямую, без лишних приседаний и усложнения сборки.
Важно отметить, что это именно шаблон, а не догма. При необходимости базовый дистрибутив можно без проблем заменить на Debian, Ubuntu или любой другой, сократив набор зависимостей и адаптировав Dockerfile под конкретные требования проекта или production-окружения.
Двигаемся дальше и переходим к системной подготовке контейнера:
RUN echo "* soft core 0" >> /etc/security/limits.conf && \ echo "* hard core 0" >> /etc/security/limits.conf && \ echo "* soft nofile 10000" >> /etc/security/limits.conf
Здесь задаём системные лимиты контейнера. Отключаем создание core-дампов, которые в большинстве случаев не нужны, и увеличиваем лимит на количество открытых файлов. Это снижает риск неожиданных runtime-ошибок и делает поведение приложения более стабильным под нагрузкой.
RUN sed -i 's/^UID_MAX.*/UID_MAX 999999999/' /etc/login.defs RUN sed -i 's/^GID_MAX.*/GID_MAX 999999999/' /etc/login.defs RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
Настраиваем системные параметры пользователей и времени. Увеличение UID_MAX и GID_MAX необходимо для работы в средах с удалёнными dev-боксами и кластерами, где идентификаторы пользователей часто выбираются из большого, нефиксированного диапазона. Такая настройка позволяет сборкам корректно работать при пробросе UID/GID хоста и избежать проблем с правами доступа. Установка таймзоны делает логи и поведение приложения более предсказуемыми.
RUN set -eux; \ groupadd $DOCKER_USER --gid=$DOCKER_HOST_GID && \ useradd --no-log-init -g $DOCKER_USER --uid=$DOCKER_HOST_UID \ -d $DOCKER_USER_HOME -ms /bin/bash $DOCKER_USER RUN mkdir /application && chown $DOCKER_USER:$DOCKER_USER /application
Подготавливаем окружение для работы внутри контейнера от непривилегированного пользователя. Это базовая практика безопасности и удобства: процессы не запускаются от root, а файлы не создают проблем с правами доступа на хосте. Пользователь создаётся с фиксированными UID/GID и заранее подготовленным рабочим каталогом /application, который далее будет монтироваться как рабочая директория проекта. Эта заготовка используется во всех следующих стадиях сборки, где мы будем устанавливать зависимости, подключать dev-инструменты и запускать приложение.
Настриваем зеркала Arch Linux и ставим системные зависимости, необходимые для дальнейшей сборки образа:
RUN set -eux; \ tmp="$(mktemp)"; \ if curl -fsSL \ --connect-timeout 10 \ --max-time 30 \ --retry 5 \ --retry-delay 1 \ --retry-all-errors \ "https://archlinux.org/mirrorlist/?country=${MIRROR_LIST_COUNTRY}&protocol=https&ip_version=4&use_mirror_status=on" \ | sed -e 's/^\s*#Server/Server/' -e '/^\s*#/d' \ > "$tmp" \ && grep -q '^Server' "$tmp"; then \ mv "$tmp" /etc/pacman.d/mirrorlist; \ else \ echo "WARN: mirrorlist update failed; keeping existing /etc/pacman.d/mirrorlist" >&2; \ rm -f "$tmp"; \ fi RUN pacman -Syu --noconfirm && \ pacman -S --noconfirm --needed $BUILD_PACKAGES && \ pacman -Scc --noconfirm && \ rm -rf /var/lib/pacman/sync/*
Дикий RUN с curl только на первый взгляд выглядит пугающе. Его задача простая: попытаться получить актуальный список зеркал Arch Linux для нужного региона и использовать их для установки пакетов. Делается несколько попыток с таймаутами и ретраями, потому что archlinux.org время от времени бывает недоступен (иногда по вполне понятным причинам вроде DDoS).
Если обновить список зеркал не удалось, то сборка не падает. В этом случае используется стандартный mirrorlist, уже встроенный в базовый образ. Такой подход делает сборку более устойчивой: мы ускоряем загрузку пакетов, когда всё работает нормально, но не ломаем процесс, если внешний сервис временно недоступен.
Дальше обновляем систему (у нас rolling-release!), устанавливаем набор системных зависимостей, заданный отдельной переменной и необходимый в основном для работы Python-пакетов. После установки кеш pacman очищается, чтобы не увеличивать размер итогового образа.
В dev-контейнере иногда требуется быстро установить или проверить что-то вручную, не пересобирая образ. Эта настройка позволяет пользователю выполнять команды с sudo без ввода пароля, что заметно упрощает такие временные эксперименты (в app сборке уберём):
RUN echo "${DOCKER_USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
В этом блоке через pyenv ставим нужную версию Python и задаём эту версию как глобальную внутри контейнера:
ENV PYENV_ROOT=$DOCKER_USER_HOME/.pyenv ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH RUN pyenv install --skip-existing $PYTHON_VERSION && \ pyenv global $PYTHON_VERSION && \ pyenv rehash && \ rm -rf "$PYENV_ROOT/cache" "$PYENV_ROOT/sources" /tmp/python-build*
Для установки Python используем pyenv. Такой подход позволяет легко менять версию Python, просто изменив одно значение в переменных сборки, без переписывания Dockerfile. Это удобно при поддержке нескольких проектов или тестировании на разных версиях интерпретатора.
Минус у этого решения тоже есть: итоговый образ может получаться немного больше по размеру по сравнению с вариантами на готовых Python-образах. Тем не менее, в контексте шаблона удобство и гибкость смены версии Python зачастую перевешивают этот недостаток и заметно экономят время в повседневной работе.
Дальше настраиваем Python: виртуальное окружение, переменные для работы и Poetry как основной инструмент управления зависимостями:
ENV PYTHONUNBUFFERED=1 ENV PIP_DEFAULT_TIMEOUT=100 ENV POETRY_NO_INTERACTION=1 ENV POETRY_HOME=/opt/poetry ENV POETRY_CACHE_DIR=/var/cache/pypoetry ENV PIP_CACHE_DIR=/var/cache/pip ENV VIRTUAL_ENV=/opt/venv RUN python -m venv --copies $VIRTUAL_ENV ENV PATH=$VIRTUAL_ENV/bin:$PATH RUN pip install --upgrade pip RUN curl -sSL https://install.python-poetry.org | POETRY_VERSION=$POETRY_VERSION python - ENV PATH=$POETRY_HOME/bin:$PATH ENV PYTHONPATH=/application/src ENV PROJECT_ROOT=/application ENV HOME=$DOCKER_USER_HOME
Здесь всё достаточно стандартно для Python-проектов. Мы создаём отдельное виртуальное окружение внутри контейнера и настраиваем переменные, отвечающие за предсказуемую работу Python, pip и Poetry. Зависимости изолируем в venv, а кеши pip и Poetry выносим в отдельные каталоги, что удобно для повторных сборок и dev-сценариев. Poetry устанавливаем как основной инструмент управления зависимостями без интерактивных запросов, а базовые переменные вроде PYTHONPATH и PROJECT_ROOT упрощают дальнейшую работу с кодом и инструментами.
На этом этапе у нас готова базовая заготовка: воспроизводимое Python-окружение с Poetry, настроенным пользователем и системной базой. Этот слой служит фундаментом для всех последующих сборок. Дальше мы рассмотрим образы, которые собираются поверх этой базы и решают уже конкретные задачи — от установки зависимостей приложения до dev- и IDE-окружений.
Multistage-сборки поверх базового образа
Теперь давайте дополним наш Dockerfile и посмотрим на производные сборки, которые используют базовый образ python-base. Они позволяют из одного Dockerfile собрать образы для разных сценариев использования.
Начнём с образа для работы с зависимостями:
FROM python-base AS poetry ARG DOCKER_HOST_UID=1000 ARG DOCKER_HOST_GID=1000 ARG DOCKER_USER=devuser RUN mkdir -p $POETRY_CACHE_DIR && \ chown -R $DOCKER_USER $POETRY_CACHE_DIR RUN mkdir -p $PIP_CACHE_DIR && \ chown -R $DOCKER_USER $PIP_CACHE_DIR USER ${DOCKER_HOST_UID}:${DOCKER_HOST_GID} WORKDIR /application
Этот образ специально делаем лёгким. Он не привязан к выполнению poetry install, поэтому можно свободно менять pyproject.toml и пересобирать актуальный poetry.lock, не затрагивая остальные этапы сборки. Созданные каталоги кешей pip и Poetry мы позже привяжем к локальным томам в docker compose. Это позволит переиспользовать кеш между перезапусками контейнеров и заметно ускорить установку зависимостей при разработке.
Посмотрим внимательнее на этот блок:
... ARG DOCKER_HOST_UID=1000 ARG DOCKER_HOST_GID=1000 ... USER ${DOCKER_HOST_UID}:${DOCKER_HOST_GID} ...
В последующих сборках этот приём будет повторяться — мы везде переключаемся на пользователя по UID/GID, а не по имени. Формально можно было бы использовать USER $DOCKER_USER, но в CI-сценариях с кешированием слоёв (например, при сборке через kaniko) это иногда приводит к ошибкам, так как Docker пытается разрешить пользователя через /etc/passwd, которого может не быть в кеше. Использование UID/GID делает сборку более устойчивой и предсказуемой в автоматизированных пайплайнах.
Идём дальше, к следующей сборке. Этот образ служит заготовкой для прод приложения и содержит только необходимые зависимости без dev-инструментов:
FROM python-base AS app-build ARG DOCKER_HOST_UID=1000 ARG DOCKER_HOST_GID=1000 ARG DOCKER_USER=devuser COPY src/ build/src COPY README.md /build/ COPY pyproject.toml poetry.lock /build/ ARG POETRY_OPTIONS_APP="--only main --compile" RUN poetry install $POETRY_OPTIONS_APP -n -v -C /build && \ rm -rf $POETRY_CACHE_DIR/* && rm -rf $PIP_CACHE_DIR/* RUN sed -i "/^${DOCKER_USER}[[:space:]]/d" /etc/sudoers USER ${DOCKER_HOST_UID}:${DOCKER_HOST_GID} WORKDIR /application
Здесь мы копируем исходный код и файлы проекта, устанавливаем только прод зависимости через Poetry и сразу очищаем кеши. Дополнительно убираем sudo, так как в production-образе он не нужен, переключаемся на непривилегированного пользователя.
А это самая интересная часть - dev-сборка, где мы устанавливаем зависимости для разработки, дополнительные инструменты вроде Vim, Node и AI-утилит и подготавливаем полноценное рабочее окружение внутри контейнера:
FROM python-base AS build-deps-dev ARG DOCKER_USER=devuser ARG VIM_PACKAGES="python vim ctags ripgrep bat npm nodejs-lts-jod openai-codex gemini-cli" ARG POETRY_OPTIONS_DEV="--no-root --with-dev --compile" RUN pacman -Sy --noconfirm && \ pacman -S --noconfirm --needed $VIM_PACKAGES && \ pacman -Scc --noconfirm && \ rm -rf /var/lib/pacman/sync/* COPY pyproject.toml poetry.lock /build/ RUN poetry install $POETRY_OPTIONS_DEV -n -v -C /build && \ rm -rf $POETRY_CACHE_DIR/* $PIP_CACHE_DIR/* RUN mkdir -p $POETRY_CACHE_DIR $PIP_CACHE_DIR && \ chown -R $DOCKER_USER $POETRY_CACHE_DIR $PIP_CACHE_DIR RUN mkdir -p $DOCKER_USER_HOME/.codex && \ chown -R $DOCKER_USER $DOCKER_USER_HOME/.codex RUN mkdir -p $DOCKER_USER_HOME/.gemini && \ chown -R $DOCKER_USER $DOCKER_USER_HOME/.gemini RUN mkdir -p $DOCKER_USER_HOME/.config && \ chown -R $DOCKER_USER $DOCKER_USER_HOME/.config
За название аргумента VIM_PACKAGES не судите строго. Изначально шаблон развивался как сборка Vim-IDE внутри контейнера, и это имя осталось как рудимент ранних версий. Отдельно стоит обратить внимание на готовые пакеты codex и gemini. Их установка через системный пакетный менеджер даёт заметно меньший размер образа по сравнению с установкой через npm, а также упрощает и ускоряет сборку dev-окружения.
Созданные каталоги для codex, gemini и пользовательских конфигураций далее будут смонтированы в локальные тома в docker compose. Это позволяет переиспользовать учётные данные и настройки AI-агентов между перезапусками контейнеров и не настраивать их заново каждый раз.
Здесь мы делаем заготовку для будущих dev-сборок: переключаемся на непривилегированного пользователя и задаём рабочий каталог контейнера:
FROM build-deps-dev AS dev-build ARG DOCKER_HOST_UID=1000 ARG DOCKER_HOST_GID=1000 ARG DOCKER_USER=devuser USER ${DOCKER_HOST_UID}:${DOCKER_HOST_GID} WORKDIR /application
Сборку Vim-IDE я намеренно убираю под кат. Это достаточно специфичная часть шаблона и нужна далеко не всем. Здесь настраивается полноценная IDE на базе Vim с плагинами, LSP и дополнительными инструментами. Подробно разбирать этот слой мы не будем - при необходимости все детали настройки можно посмотреть в README репозитория шаблона.
Скрытый текст
FROM build-deps-dev AS vim-ide ARG DOCKER_HOST_UID=1000 ARG DOCKER_HOST_GID=1000 ARG DOCKER_USER=devuser ARG DOCKER_USER_HOME=/home/devuser USER ${DOCKER_HOST_UID}:${DOCKER_HOST_GID} RUN curl -fLo $DOCKER_USER_HOME/.vim/autoload/plug.vim --create-dirs \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim RUN curl -fLo $DOCKER_USER_HOME/.vim/spell/en.utf-8.spl \ --create-dirs https://ftp.nluug.nl/pub/vim/runtime/spell/en.utf-8.spl RUN curl -fLo $DOCKER_USER_HOME/.vim/spell/en.utf-8.sug \ --create-dirs https://ftp.nluug.nl/pub/vim/runtime/spell/en.utf-8.sug RUN curl -fLo $DOCKER_USER_HOME/.vim/spell/ru.utf-8.spl \ --create-dirs https://ftp.nluug.nl/pub/vim/runtime/spell/ru.utf-8.spl RUN curl -fLo $DOCKER_USER_HOME/.vim/spell/ru.utf-8.sug \ --create-dirs https://ftp.nluug.nl/pub/vim/runtime/spell/ru.utf-8.sug COPY --chown=$DOCKER_USER:$DOCKER_USER .vimrc $DOCKER_USER_HOME/.vimrc RUN cat $DOCKER_USER_HOME/.vimrc \ |sed -n '/plug#begin/,/plug#end/p' > $DOCKER_USER_HOME/.vimrc_plug RUN vim -u $DOCKER_USER_HOME/.vimrc_plug +'PlugInstall --sync' +qa RUN vim -u $DOCKER_USER_HOME/.vimrc_plug \ +'CocInstall -sync coc-pyright coc-json coc-yaml coc-snippets coc-markdownlint' +qa COPY --chown=$DOCKER_USER:$DOCKER_USER .coc-settings.json \ $DOCKER_USER_HOME/.vim/coc-settings.json RUN git config --global --add safe.directory /application ENV TERM=xterm-256color WORKDIR /application
Почему я всё-таки упоминаю эту сборку? Потому что это наглядный пример того, как можно дальше кастомизировать dev-сборки, добавляя специализированные окружения поверх build-deps-dev под конкретные задачи и рабочие привычки.
В итоге из одного Dockerfile мы получили несколько сборок под разные задачи: прод, dev и специализированные окружения. Базу не трогаем, нужное просто надстраиваем — аккуратно и без лишней магии. А теперь давайте всё это соберём.
docker compose: один файл - много сценариев
Для сборок и запусков нам понадобится .env-файл — это место, где удобно управлять всеми настройками. Здесь задаются версии Python и Poetry, набор системных пакетов, параметры окружения и секреты вроде API-ключей. Меняя значения в .env, можно адаптировать весь проект под другую машину или сценарий запуска, не трогая Dockerfile и docker-compose.
.env.dist
# compose env file COMPOSE_PROJECT_NAME="python-docker-template" TZ=Asia/Vladivostok DOCKER_HOST_UID=1000 DOCKER_HOST_GID=1000 DOCKER_USER=developer DOCKER_USER_HOME=/home/developer DOCKER_PLATFORM=linux/amd64 MIRROR_LIST_COUNTRY=RU BUILD_PACKAGES="pyenv git gnupg sudo postgresql-libs mariadb-libs openmp" VIM_PACKAGES="python vim ctags ripgrep bat npm nodejs-lts-jod openai-codex gemini-cli" PYTHON_VERSION=3.14 PYTHONUNBUFFERED=1 PIP_DEFAULT_TIMEOUT=100 POETRY_VERSION=2.2.1 POETRY_OPTIONS_APP="--only main --compile" POETRY_OPTIONS_DEV="--no-root --with dev --compile" POETRY_NO_INTERACTION=1 JUPYTER_TOKEN=_change_me_please_!1_ OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxx" GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
Отдельно стоит обратить внимание на DOCKER_HOST_UID и DOCKER_HOST_GID. Эти значения важно задать корректно под вашего пользователя на хосте — от них зависит, с какими правами контейнер будет работать с примонтированными файлами. На Linux и macOS их можно посмотреть командой:
id
Если UID/GID совпадают, вы избавляетесь от классических проблем с правами доступа и «root-файлами» в рабочем каталоге.
Для начала посмотрим на общие настройки и якоря compose, через которые мы централизованно прокидываем переменные окружения и аргументы сборки во все сервисы:
Скрытый текст
x-default-env: &default-env POETRY_NO_INTERACTION: "1" PIP_DEFAULT_TIMEOUT: "100" PYTHONUNBUFFERED: "1" x-platform: &platform ${DOCKER_PLATFORM:-linux/amd64} x-default-args: &default-args TZ: ${TZ:-Asia/Vladivostok} DOCKER_HOST_UID: ${DOCKER_HOST_UID:-1000} DOCKER_HOST_GID: ${DOCKER_HOST_GID:-1000} DOCKER_USER: ${DOCKER_USER:-developer} DOCKER_USER_HOME: ${DOCKER_USER_HOME:-/home/developer} MIRROR_LIST_COUNTRY: ${MIRROR_LIST_COUNTRY:-RU} BUILD_PACKAGES: ${BUILD_PACKAGES:-pyenv git gnupg sudo postgresql-libs mariadb-libs openmp} VIM_PACKAGES: ${VIM_PACKAGES:-python vim ctags ripgrep bat npm nodejs-lts-jod openai-codex gemini-cli} PYTHON_VERSION: ${PYTHON_VERSION:-3.14} POETRY_VERSION: ${POETRY_VERSION:-2.2.1} POETRY_OPTIONS_APP: ${POETRY_OPTIONS_APP:---only main --compile} POETRY_OPTIONS_DEV: ${POETRY_OPTIONS_DEV:---no-root --with dev --compile}
compose по умолчанию читает значения переменных из .env-файла, а если каких-то параметров там нет, используются значения по умолчанию, заданные прямо в конфигурации.
Теперь кратко рассмотрим сервисы, описанные в compose, и какие задачи каждый из них решает.
сервисы в compose.yaml
services: python-base: platform: *platform build: target: python-base args: *default-args environment: <<: *default-env poetry: platform: *platform entrypoint: poetry build: target: poetry args: *default-args environment: <<: *default-env volumes: - type: bind source: . target: /application - type: volume source: pip-cache target: /var/cache/pip - type: volume source: poetry-cache target: /var/cache/pypoetry vim-ide: platform: *platform entrypoint: vim build: target: vim-ide args: *default-args environment: <<: *default-env OPENAI_API_KEY: ${OPENAI_API_KEY} GEMINI_API_KEY: ${GEMINI_API_KEY} volumes: - type: bind source: . target: /application - type: volume source: gemini-auth target: ${DOCKER_USER_HOME}/.gemini - type: volume source: codex-auth target: ${DOCKER_USER_HOME}/.codex gemini: platform: *platform entrypoint: gemini build: target: dev-build args: *default-args environment: <<: *default-env GEMINI_API_KEY: ${GEMINI_API_KEY} volumes: - type: bind source: . target: /application - type: volume source: gemini-auth target: ${DOCKER_USER_HOME}/.gemini codex: platform: *platform entrypoint: codex build: target: dev-build args: *default-args environment: <<: *default-env OPENAI_API_KEY: ${OPENAI_API_KEY} volumes: - type: bind source: . target: /application - type: volume source: codex-auth target: ${DOCKER_USER_HOME}/.codex codex-web-login: platform: *platform entrypoint: codex login build: target: dev-build args: *default-args environment: <<: *default-env network_mode: host volumes: - type: volume source: codex-auth target: ${DOCKER_USER_HOME}/.codex jupyterlab: platform: *platform entrypoint: - jupyter-lab - --port=8888 - --ip="0.0.0.0" - --no-browser - --IdentityProvider.token=${JUPYTER_TOKEN} ports: - "8888:8888" build: target: dev-build args: *default-args environment: <<: *default-env volumes: - type: bind source: . target: /application app: platform: *platform entrypoint: template_bin build: target: app-build args: *default-args environment: <<: *default-env volumes: pip-cache: driver: local poetry-cache: driver: local codex-auth: driver: local gemini-auth: driver: local
Вся конфигурация сделана так, чтобы для работы с проектом было достаточно одной команды вида docker compose run --rm <service_name>. Если собрать всё, что мы разобрали выше, то использование шаблона сводится к нескольким понятным сценариям.
Инициализация проекта начинается с правок в .env. Здесь мы задаём версии Python и Poetry, UID/GID пользователя, системные пакеты и, при необходимости, API-ключи. После этого вся дальнейшая работа идёт через compose, без ручной настройки окружения на хосте, например:
cp .env.dist .env docker compose build vim-ide docker compose run --rm vim-ide
Работа с зависимостями вынесена в отдельный сервис, сначала обновляем pyproject.toml, затем пересобираем poetry.lock:
docker compose run --rm poetry lock
Важно помнить, что после обновления зависимостей (изменений в pyproject.toml или poetry.lock) соответствующие образы нужно пересобрать. Обычно это делается точечно - только для тех сервисов, которые эти зависимости используют:
docker compose build <service_name>
Например, после изменения зависимостей потребуется пересобрать vim-ide, dev-build или app, в зависимости от того, какой сценарий вы планируете запускать дальше.
Jupyter и эксперименты - отдельный сценарий, но всё в том же окружении. Код примонтирован как volume, зависимости уже установлены, права совпадают с хостом можно работать сразу:
docker compose run --rm --service-ports jupyterlab
А прод сборка - это уже минимальный образ без dev-инструментов:
docker compose build app docker compose run --rm app
Ну куда же сейчас без AI-агентов - конечно, и в этом шаблоне они тоже есть. Правда, без лишней магии: здесь они представлены в виде CLI-инструментов вроде Codex и Gemini.
Запуск этих инструментов внутри контейнера даёт приятный бонус: изоляцию и безопасность. CLI работает только с теми файлами проекта, которые примонтированы в контейнер, и не имеет доступа к остальной файловой системе хоста. Это снижает риски, упрощает контроль над тем, что именно видит инструмент, и делает такой workflow более предсказуемым.
При этом подход остаётся удобным: AI-утилиты запускаются из консоли, в том же окружении, где живёт код и зависимости проекта. Их можно использовать как вспомогательный инструмент — подсмотреть структуру проекта, задать вопрос по файлу или быстро накидать заготовку — не выходя из привычного рабочего процесса.
Дальше мы посмотрим, как именно запускаются Codex и Gemini, как устроена авторизация и почему всё это не требует повторной настройки при каждом запуске контейнера.
Если вы богаты, успешны и у вас уже есть API-ключи для OpenAI и Gemini — всё максимально просто. Достаточно прописать их в .env:
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx GEMINI_API_KEY=your_gemini_api_key
После этого AI-инструменты готовы к работе без дополнительных шагов. Запуск сводится к одной команде:
docker compose run --rm codex # или docker compose run --rm gemini
В следующем шаге разберём более «приземлённый» сценарий — когда API-ключей нет, но попробовать всё равно хочется.
Для Codex используется авторизация через браузер (для платных подписок chatgpt), без явного API-ключа. Достаточно запустить специальный сервис:
docker compose run --rm codex-web-login
Переходите по ссылке в браузере на вашем хосте, залогинитесь в OpenAI, а полученные токены сохранятся в volume. После этого Codex можно запускать обычным способом:
docker compose run --rm codex
Для Gemini всё ещё проще — при первом запуске:
docker compose run --rm gemini
достаточно выбрать вход через Google-аккаунт и вбить токен выданный на странице. Важно что бы аккаунт был "личный", то есть без привязки к workspace.
В обоих случаях токены и настройки живут отдельно от контейнеров и не теряются между перезапусками, а сам workflow остаётся тем же самым: docker compose run --rm <service>.
Вместо выводов
В итоге этот шаблон - не попытка изобрести универсальный «правильный Docker для всех», а практичная заготовка, которую автор использует в первую очередь для DS/ML-сценариев. Там, где код часто запускается на удалённых машинах, в кластерах или на специфичном железе, особенно важно, чтобы окружение было воспроизводимым и одинаковым везде.
Да, можно долго оптимизировать размер образов, но в реальной жизни эта экономия обычно рушится в тот момент, когда в проекте появляется tensorflow или pytorch. Поэтому здесь сделан осознанный выбор в пользу удобства, гибкости и скорости работы, а не борьбы за каждый мегабайт.
Все сборки и сценарии обкатаны в боевых условиях и действительно экономят время - как на старте проекта, так и при ежедневной работе. А дальше всё просто: это всего лишь шаблон. Его можно упрощать, усложнять, менять базовый дистрибутив, убирать Vim, добавлять свои сервисы - подстраивайте его под себя и свой workflow, а не наоборот.
Если этот шаблон сэкономит вам хотя бы пару часов — значит, он уже отработал своё.
