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

CrazyHackGUT
04.01.2026 14:20Как вы планируете решать недоступность archlinux.org в случаях когда домашний провайдер ограничивает доступ к нему потому что Роскомнадзор?
Это если что не выдуманный пример, ресурсы Арча сидят на инфраструктуре Хецнера, к которой в том или ином виде еще год назад Дом.ру ограничивал доступ.

jamm1985 Автор
04.01.2026 14:20Если честно, то я пока ни разу не сталкивался с блокировкой хостинга archlinux.org со стороны своего провайдера (РТ). Но, конечно же, такое может случиться.
Если запрос зеркал не отработает, то в контейнере остануться зеркала по умолчанию. Сейчас это:
Server =https://fastly.mirror.pkgbuild.com/arch
Server =https://geo.mirror.pkgbuild.com/arch
По whois, похоже, это один и тот же провайдер.
В качестве решения можно задать зеркала в отдельном конфиге (например, зеркало Яндекса). И на сборке заменять
/etc/pacman.d/mirrorlistбез динамического резолвинга зеркал по стране. В этом случае, докерфайл получится короче, но нужно следить за актуальносью зеркал.

Elaugaste
04.01.2026 14:20Зачем внутри контейнера нужен venv, это и так изолированная среда?
Зачем копировать src до вызова установки зависимостей, это решение приводит к тому что любое изменение кода приводит к необходимости пересобирать слой, вместо того чтобы взять слой с уже установленными зависимостями? Для ci пайплайнов такие упущения ведут к долгим пересборкам контейнеров, а они по хорошему не должны занимать дольше пары минут (нужно максимально использовать кеш слоев и кеш самого poetry).
Снятие лимитов тоже крайне сомнительная история, лимиты не просто так придуманы, они страхуют от ошибок. Например чтобы приложение не на открывало несколько десятков тысяч коннектов, из за какого нибудь бага. Для прода это особенно спорная история.

jamm1985 Автор
04.01.2026 14:20Зачем внутри контейнера нужен venv, это и так изолированная среда?
Хорший вопрос. Могу поделиться интуицией. venv здесь используется не для изоляции, а для структуры и предсказуемости. venv вместе с целевой версией питона выделяет окружение проекта в отдельный, хорошо определённый слой. Это упрощает контроль зависимостей, хорошо интегрируется с poetry и делает поведение окружения более стабильным в мультистадиных сборках. В таком подходе зависимости проекта не смешиваются с системным питоном и не протекают между стадиями сборки.
Кроме того, на практике многие инструменты, типа, линтеры, чекеры, форматтеры и автокомплитеры (black, pylint, ruff, lsp и т. д.) ожидают наличие виртуального окружения или, по крайней мере, работают с ним более предсказуемо.
Зачем копировать src до вызова установки зависимостей
poetry install в режиме установки root пакета требует наличия readme и исходного кода проекта. Поэтому на этом этапе код копируется заранее. Подумаю как разделить, но вызовов poetry install будет два...
Снятие лимитов тоже крайне сомнительная история
В целом замечание справедливое. Вопрос здесь скорее в том, какие значения задавать по умолчанию для шаблона. На практике я часто их отключаю для dev сборок, что бы не натыкаться переодически на ограничения и не пересобирать всё. Наверное, стоит добавить комментарий в документацию по этому поводу, что настройки лимитов не являются рекомендацией для прода, а лишь удобной стартовой точкой.
Tishka17
точно воспроизводимое?
прям две субд сразу используете в проде?
а зачем ставили?
jamm1985 Автор
На мой взгляд воспроизводимое. pacman -Syu больше для безопасности, по размеру это с десяток мегабайт, базовые пакеты не так часто обноаляются, но что бы установка зависимостей прошла гарантировано без сбоев лучше убедиться что всё в актуальном состоянии.
Это пример установки библиотек в шаблоне. Эти библиотеки вообще можно убрать из .env если python пакеты не имеют от них зависимостей. На практике DS/ML проекты не обходятся без клиентов к БД, поэтому решил оставить для иллюстрации.
sudo остаётся для dev сборок. Иногда по зависитям что то не работает сразу. Удобно в контейнере что то доставить, проверить и потом перенести решение в сборки за один раз.