Как собрать Docker-образ, который можно запускать в проде (а не только у себя на ноуте)
- четверг, 12 июня 2025 г. в 00:00:10
Если ты пишешь Dockerfile
, скорее всего, он работает. Но вопрос не в том, работает ли. Вопрос в другом: будет ли он работать через неделю, на другом сервере, в CI/CD, на чужом железе — и будет ли это безопасно. Или всё сломается, потому что ты не зафиксировал зависимости, положился на latest
, и забыл про то, что ENTRYPOINT
— это тоже код.
В этой статье — как собрать нормальный Docker-образ, который предсказуем, устойчив и готов к продакшену.
Многие берут базовый образ, не задумываясь. Например, python:3.12
. Это "толстый" образ с кучей ненужных пакетов. Он может весить 1+ ГБ. Там куча системных библиотек, что увеличивает потенциальную поверхность для атак и делает билды медленнее.
Плохой Dockerfile
:
FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "main.py"]
Что тут не так:
Образ большой.
Все слои кэша сбиваются при любом изменении.
Ты клонируешь весь проект внутрь, включая мусор.
Как лучше:
Брать python:3.12-slim
или python:3.12-alpine
, но только если понимаешь, как с ним работать и для чего ты его выбираешь.
Использовать многоступенчатую сборку: сначала билд, потом перенос нужного в "чистую" фазу.
Указывать чёткую структуру слоёв: сначала зависимости, потом код.
latest
— это ловушка. Сегодня образ один, завтра он обновится, и всё сломается. Причём сломается неожиданно — у тебя в проде или в CI.
Пример:
FROM node:latest
В понедельник это Node 18. В среду — уже 20. И твой build падает, потому что какой-то пакет несовместим.
Лучше указывать конкретную версию:
FROM node:18.16.1
Или даже с sha256:
FROM node@sha256:<digest>
Это не просто паранойя. Это способ зафиксировать окружение. Больше контроля — меньше сюрпризов.
FROM python:3.12.1-slim as builder
WORKDIR /install
RUN apt-get update && apt-get install -y build-essential
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip wheel --no-deps --wheel-dir /wheels -r requirements.txt
FROM python:3.12.1-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-deps --no-index --find-links=/wheels -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Этот Dockerfile делает две вещи: сначала собирает зависимости, потом создаёт чистый образ с приложением. Так получается меньше мусора и быстрее сборка.
Первая часть — сборка зависимостей:
Берём лёгкий образ Python 3.12.1
Ставим build-essential — он нужен для компиляции некоторых пакетов
Копируем файл requirements.txt
Собираем зависимости в wheel-файлы (это как zip-архивы для Python-пакетов)
Фишка в том, что build-essential останется только в этой временной части и не попадёт в итоговый образ.
Вторая часть — финальный образ:
Снова берём чистый Python 3.12.1 без лишнего
Настраиваем две важные переменные:
PYTHONDONTWRITEBYTECODE — чтобы не создавались .pyc-файлы
PYTHONUNBUFFERED — чтобы логи выводились сразу
Копируем wheel-файлы из первой части
Ставим зависимости из этих файлов (уже без интернета и компиляции)
Копируем само приложение
Запускаем uvicorn на порту 8000
Почему так лучше:
Итоговый образ меньше весит
Не нужно компилировать пакеты при каждом запуске
Сборка идёт быстрее за счёт кеширования wheel-файлов
Нет лишних пакетов типа build-essential в финальном образе
Если нужно добавить что-то в образ (например, curl для healthcheck), делай это перед последним COPY. Но помни — каждый RUN добавляет слой к образу.
Без .dockerignore
ты случайно кладёшь в образ лишнее:
.git
pycache
.env
.vscode/
node_modules/
и т.д. и т.п.
Это увеличивает размер, сбивает кеш, и вообще плохо.
Пример .dockerignore
:
.git
__pycache__/
*.pyc
.env
.vscode/
node_modules/
*.log
Сделай это один раз — и забудешь про проблемы.
Проблема: когда ты монтируешь volume, он затирает всё внутри контейнера.
docker run -v $(pwd):/app myapp
В результате:
Всё, что ты собрал — перезаписано.
Права доступа могут сломать работу.
Что делать:
Используй именованные volume:
docker volume create mydata
docker run -v mydata:/data myapp
Пропиши правильные права заранее.
Нюанс: CMD подменяется, ENTRYPOINT — остаётся.
Пример:
CMD ["python", "app.py"]
Ты запустил:
docker run myapp bash
А получил: python app.py bash
Правильно так:
ENTRYPOINT ["python", "app.py"]
CMD ["--debug"]
Теперь всё работает предсказуемо.
По умолчанию Docker запускается от root. Это опасно. Особенно если есть volume или доступ к сокету.
Добавь:
RUN useradd -m myuser
USER myuser
Теперь всё работает от безопасного пользователя. И если кто-то сломает контейнер, он не получит root.
Docker считает, что контейнер жив, если он просто не умер. Но твой сервис может зависнуть или вернуть 500.
Добавь healthcheck:
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
Теперь Docker будет знать, когда сервис реально жив.
Слои в Docker кешируются. Если ты пишешь слои неправильно, кеш не работает.
Плохо:
COPY . .
RUN pip install -r requirements.txt
Лучше:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
Теперь, если ты меняешь только код, зависимости не ставятся заново.
Частые ошибки:
Пуш latest
без тега.
Кладут приватные ключи в образ.
Не используют .dockerignore
.
Как надо:
Указывать теги явно: myapp:1.2.3
Не хранить секреты в образе. Использовать secrets или переменные окружения.
Убедиться, что .dockerignore
есть и рабочий.
Каждый MB важен, особенно в CI/CD. Что можно сделать:
Убирать временные пакеты после использования.
RUN apt-get install -y gcc && pip install some-lib && apt-get remove -y gcc
Использовать --no-cache
и чистить /tmp
.
Применять slim
, а не full-образы.
Да, он маленький. Но:
Использует musl, а не glibc.
Проблемы с совместимостью C-библиотек.
Долгий билд Python‑пакетов.
Используй Alpine только если ты понимаешь, зачем он тебе нужен. В остальных случаях — slim
.
Сборка Docker-образа — это не только про "работает ли у меня". Это про устойчивость, безопасность и предсказуемость. Маленькие ошибки приводят к большим проблемам.
Используй лучшие практики — и будет меньше боли.
Если есть что добавить или считаешь что что-то из мной перечисленного можно отнести к "вредным советам" - милости прошу в комментарии. Я прочитаю и отредактирую, если твое предложение будет более актуально.