# Docker: база, которой хватит

> Образ, слои и кэш, multi-stage, Dockerfile для Python и Node, non-root, .dockerignore

_Источник: https://sii.sergeivolchkov.ru/materials/docker/basics_



Docker в курсе нужен ради одной фразы: **«работает у меня» перестаёт быть
аргументом**. С первой недели проект поднимается одной командой — у тебя,
у одногруппника, на проде и в CI одинаково.

Здесь только та база, которая реально понадобится в проекте. Всё
остальное про Docker выучится по дороге, когда прижмёт.

## Образ, контейнер, слой [#образ-контейнер-слой]

* **Образ** — неизменяемый слепок файловой системы с описанием запуска.
  Собирается из `Dockerfile`, лежит в реестре, имеет тег.
* **Контейнер** — запущенный экземпляр образа плюс тонкий изменяемый слой
  сверху. Удалил контейнер — изменения этого слоя ушли.
* **Слой** — результат одной инструкции `Dockerfile`. Слои кэшируются
  и переиспользуются, и именно из-за них порядок строк важнее их содержимого.

Отсюда главное правило сборки: **сначала то, что меняется редко, потом то,
что меняется каждый коммит**. Зависимости ставятся до копирования кода —
иначе каждая правка строчки инвалидирует установку пакетов.

```dockerfile
# Плохо: любая правка кода = переустановка всех зависимостей
COPY . .
RUN uv sync

# Хорошо: пересборка только когда изменился манифест
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
COPY . .
```

## Multi-stage: в прод едет только результат [#multi-stage-в-прод-едет-только-результат]

<MultiStage />

Компиляторы, dev-зависимости и кэши нужны только на сборке. Multi-stage
оставляет их в стадии `builder`, а в финальный образ копирует один
артефакт — виртуальное окружение или собранный `dist`.

Что это даёт: образ заметно меньше, в проде нет ни компилятора, ни
менеджера пакетов, выкат быстрее.

**Python + uv** (тот же `uv`, что в принципах работы с агентом — один
инструмент на окружение, зависимости и lock):

```dockerfile title="Dockerfile — Python + uv"
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-install-project --no-dev
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev

FROM python:3.13-slim
WORKDIR /app
RUN useradd --create-home app
COPY --from=builder --chown=app:app /app /app
ENV PATH="/app/.venv/bin:$PATH"
USER app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```

Три неочевидные строчки: `--mount=type=cache` переживает пересборку и не
качает пакеты заново; `UV_COMPILE_BYTECODE=1` компилирует `.pyc` заранее —
контейнер стартует быстрее; `UV_LINK_MODE=copy` убирает симлинки, которые
ломаются при монтировании томов.

**Node** — та же логика, другой менеджер:

```dockerfile title="Dockerfile — Node"
FROM node:22-alpine AS builder
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.output ./
USER node
CMD ["node", "server.js"]
```

## Правила образа [#правила-образа]

1. **`.dockerignore` пишется первым.** Без него в контекст сборки уезжают
   `.git`, `node_modules`, `.venv`, датасеты — сборка тормозит, образ пухнет,
   секреты из локальных файлов утекают в слои.
2. **Не `latest`.** `python:3.13-slim`, `node:22-alpine` — конкретные теги.
   `latest` однажды поменяется, и «ничего не трогали, а сломалось».
3. **Non-root.** `USER app` в финальной стадии: процесс в контейнере не
   должен работать от рута.
4. **Секреты не в образе.** Ключи приходят переменными окружения при
   запуске. Всё, что попало в `Dockerfile` или в скопированный файл,
   лежит в слое навсегда — даже если удалить следующей инструкцией.
5. **Один процесс — один контейнер.** Бэкенд и база в одном образе
   лишают смысла и масштабирование, и обновление по частям.

## Проверь себя [#проверь-себя]

```bash
docker build -t myapp .          # собралось
docker run --rm -p 8000:8000 myapp   # запустилось и отвечает
docker images myapp              # размер — сотни МБ, а не единицы ГБ
docker history myapp             # видно, какой слой раздул образ
```

Дальше — [Compose](/materials/docker/compose): та же система, но целиком
и одной командой.
