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, а не наоборот.
Если этот шаблон сэкономит вам хотя бы пару часов — значит, он уже отработал своё.
