habrahabr

Как собрать Docker-образ, который можно запускать в проде (а не только у себя на ноуте)

  • четверг, 12 июня 2025 г. в 00:00:10
https://habr.com/ru/articles/917226/

Если ты пишешь Dockerfile, скорее всего, он работает. Но вопрос не в том, работает ли. Вопрос в другом: будет ли он работать через неделю, на другом сервере, в CI/CD, на чужом железе — и будет ли это безопасно. Или всё сломается, потому что ты не зафиксировал зависимости, положился на latest, и забыл про то, что ENTRYPOINT — это тоже код.

В этой статье — как собрать нормальный Docker-образ, который предсказуем, устойчив и готов к продакшену.


1. Первая ошибка: ты начинаешь с плохой базы

Многие берут базовый образ, не задумываясь. Например, 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, но только если понимаешь, как с ним работать и для чего ты его выбираешь.

  • Использовать многоступенчатую сборку: сначала билд, потом перенос нужного в "чистую" фазу.

  • Указывать чёткую структуру слоёв: сначала зависимости, потом код.


2. Не используй latest

latest — это ловушка. Сегодня образ один, завтра он обновится, и всё сломается. Причём сломается неожиданно — у тебя в проде или в CI.

Пример:

FROM node:latest

В понедельник это Node 18. В среду — уже 20. И твой build падает, потому что какой-то пакет несовместим.

Лучше указывать конкретную версию:

FROM node:18.16.1

Или даже с sha256:

FROM node@sha256:<digest>

Это не просто паранойя. Это способ зафиксировать окружение. Больше контроля — меньше сюрпризов.


3. Пример хорошего Dockerfile для Python‑приложения

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 делает две вещи: сначала собирает зависимости, потом создаёт чистый образ с приложением. Так получается меньше мусора и быстрее сборка.

Первая часть — сборка зависимостей:

  1. Берём лёгкий образ Python 3.12.1

  2. Ставим build-essential — он нужен для компиляции некоторых пакетов

  3. Копируем файл requirements.txt

  4. Собираем зависимости в wheel-файлы (это как zip-архивы для Python-пакетов)

Фишка в том, что build-essential останется только в этой временной части и не попадёт в итоговый образ.

Вторая часть — финальный образ:

  1. Снова берём чистый Python 3.12.1 без лишнего

  2. Настраиваем две важные переменные:

    • PYTHONDONTWRITEBYTECODE — чтобы не создавались .pyc-файлы

    • PYTHONUNBUFFERED — чтобы логи выводились сразу

  3. Копируем wheel-файлы из первой части

  4. Ставим зависимости из этих файлов (уже без интернета и компиляции)

  5. Копируем само приложение

  6. Запускаем uvicorn на порту 8000

Почему так лучше:

  • Итоговый образ меньше весит

  • Не нужно компилировать пакеты при каждом запуске

  • Сборка идёт быстрее за счёт кеширования wheel-файлов

  • Нет лишних пакетов типа build-essential в финальном образе

Если нужно добавить что-то в образ (например, curl для healthcheck), делай это перед последним COPY. Но помни — каждый RUN добавляет слой к образу.


4. .dockerignore — must-have

Без .dockerignore ты случайно кладёшь в образ лишнее:

  • .git

  • pycache

  • .env

  • .vscode/

  • node_modules/

  • и т.д. и т.п.

Это увеличивает размер, сбивает кеш, и вообще плохо.

Пример .dockerignore:

.git
__pycache__/
*.pyc
.env
.vscode/
node_modules/
*.log

Сделай это один раз — и забудешь про проблемы.


5. Volume: будь осторожен

Проблема: когда ты монтируешь volume, он затирает всё внутри контейнера.

docker run -v $(pwd):/app myapp

В результате:

  • Всё, что ты собрал — перезаписано.

  • Права доступа могут сломать работу.

Что делать:

  • Используй именованные volume:

docker volume create mydata
docker run -v mydata:/data myapp
  • Пропиши правильные права заранее.


6. CMD и ENTRYPOINT

Нюанс: CMD подменяется, ENTRYPOINT — остаётся.

Пример:

CMD ["python", "app.py"]

Ты запустил:

docker run myapp bash

А получил: python app.py bash

Правильно так:

ENTRYPOINT ["python", "app.py"]
CMD ["--debug"]

Теперь всё работает предсказуемо.


7. USER: не запускай всё от root

По умолчанию Docker запускается от root. Это опасно. Особенно если есть volume или доступ к сокету.

Добавь:

RUN useradd -m myuser
USER myuser

Теперь всё работает от безопасного пользователя. И если кто-то сломает контейнер, он не получит root.


8. Healthcheck

Docker считает, что контейнер жив, если он просто не умер. Но твой сервис может зависнуть или вернуть 500.

Добавь healthcheck:

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

Теперь Docker будет знать, когда сервис реально жив.


9. Кеш Docker

Слои в Docker кешируются. Если ты пишешь слои неправильно, кеш не работает.

Плохо:

COPY . .
RUN pip install -r requirements.txt

Лучше:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Теперь, если ты меняешь только код, зависимости не ставятся заново.


10. Docker в CI

Частые ошибки:

  • Пуш latest без тега.

  • Кладут приватные ключи в образ.

  • Не используют .dockerignore.

Как надо:

  • Указывать теги явно: myapp:1.2.3

  • Не хранить секреты в образе. Использовать secrets или переменные окружения.

  • Убедиться, что .dockerignore есть и рабочий.


11. Уменьшение размера образа

Каждый MB важен, особенно в CI/CD. Что можно сделать:

  • Убирать временные пакеты после использования.

RUN apt-get install -y gcc && pip install some-lib && apt-get remove -y gcc
  • Использовать --no-cache и чистить /tmp.

  • Применять slim, а не full-образы.


12. Alpine — не серебряная пуля

Да, он маленький. Но:

  • Использует musl, а не glibc.

  • Проблемы с совместимостью C-библиотек.

  • Долгий билд Python‑пакетов.

Используй Alpine только если ты понимаешь, зачем он тебе нужен. В остальных случаях — slim.


Заключение

Сборка Docker-образа — это не только про "работает ли у меня". Это про устойчивость, безопасность и предсказуемость. Маленькие ошибки приводят к большим проблемам.

Используй лучшие практики — и будет меньше боли.

Если есть что добавить или считаешь что что-то из мной перечисленного можно отнести к "вредным советам" - милости прошу в комментарии. Я прочитаю и отредактирую, если твое предложение будет более актуально.