Материал учебного циклаОткрытый материал

Docker: как контейнеризировать приложение и управлять его окружением

Подробное введение в Docker: образы, контейнеры, Dockerfile, слои, порты, volumes, networks, Compose, health checks, безопасность, отладка и контейнеризация FastAPI.

Инфраструктура и DevOps #DevOps #Docker #Docker Compose #Dockerfile #FastAPI #networks #volumes #контейнеры
Учебный цикл Основы API и backend-разработки Материал 7 из 8

Docker: как контейнеризировать приложение и управлять его окружением

Программа редко состоит только из исходного кода.

Для работы ей могут понадобиться:

  • определённая версия Python;
  • системные библиотеки;
  • зависимости из requirements.txt;
  • переменные окружения;
  • PostgreSQL;
  • Redis;
  • конкретная команда запуска;
  • открытый сетевой порт.

На компьютере разработчика всё это уже установлено. На сервере окружение может отличаться:

у разработчика Python 3.13 - на сервере Python 3.11

локально библиотека установлена - на сервере её нет

локально PostgreSQL доступен - на сервере другой адрес и настройки

Из-за этого появляется классическая проблема:

У меня работает, а на другом компьютере нет.

Docker помогает упаковать приложение вместе с описанием его окружения и запускать его как изолированный контейнер.

Исходный код
+
зависимости
+
системное окружение
+
команда запуска
-
воспроизводимый образ

Docker не превращает приложение в виртуальную машину и не исправляет ошибки в коде. Он задаёт повторяемый способ сборки и запуска.


Что такое контейнер

Контейнер - изолированный процесс, запущенный на хостовой операционной системе с собственным представлением:

  • файловой системы;
  • процессов;
  • сети;
  • переменных окружения;
  • пользователей;
  • ограничений ресурсов.

С точки зрения процесса внутри контейнера приложение может видеть:

/
├── app
├── usr
├── bin
└── tmp

Но это не отдельная полноценная операционная система с собственным ядром.

Контейнер использует ядро хостовой системы, а изоляция создаётся механизмами операционной системы.

Для Linux-контейнеров основными механизмами являются:

  • namespaces - разделяют процессы, сеть и другие ресурсы;
  • cgroups - учитывают и ограничивают ресурсы;
  • capabilities - делят привилегии root на отдельные возможности;
  • layered filesystem - собирает файловую систему из слоёв.

Docker скрывает большую часть низкоуровневой настройки и предоставляет единый интерфейс.


Docker и виртуальная машина

Контейнер и виртуальная машина решают похожую задачу изоляции, но на разных уровнях.

Виртуальная машина

Физический сервер
-
гипервизор
-
гостевая операционная система
-
приложение

Каждая VM обычно содержит собственное ядро и полноценную ОС.

Контейнер

Хостовая операционная система
-
Docker Engine
-
изолированный процесс

Контейнеры обычно:

  • запускаются быстрее;
  • занимают меньше места;
  • проще создаются и удаляются;
  • плотнее размещаются на одном хосте.

Но контейнеры не являются абсолютной границей безопасности. Они разделяют ядро хоста, поэтому настройки привилегий, пользователей, mounts и capabilities имеют значение.


Docker Engine и основные компоненты

Docker работает по клиент-серверной схеме.

Docker CLI

Команда:

docker run nginx

выполняется клиентом Docker.

CLI отправляет запрос Docker daemon через API.

Docker daemon

Процесс dockerd управляет:

  • образами;
  • контейнерами;
  • сетями;
  • volumes;
  • сборками;
  • API Docker Engine.

Registry

Registry хранит образы.

Примеры:

  • Docker Hub;
  • GitHub Container Registry;
  • частный registry компании.

Образ можно загрузить:

docker pull nginx

Или отправить:

docker push registry.example.com/project/api:1.0

Образ и контейнер

Эти понятия часто путают.

Docker image

Image, или образ, - неизменяемый шаблон для создания контейнеров.

Он содержит:

  • файловую систему;
  • зависимости;
  • метаданные;
  • переменные окружения по умолчанию;
  • рабочую директорию;
  • команду запуска.

Container

Контейнер - запущенный или остановленный экземпляр образа.

Из одного образа можно создать несколько контейнеров.

Аналогия:

Класс - описание структуры

Объект - конкретный экземпляр

Она не полностью технически точна, но помогает понять отношение.

Команда:

docker run nginx

в упрощённом виде делает следующее:

если образа нет - скачать
-
создать контейнер
-
добавить изменяемый слой
-
создать сеть
-
запустить основной процесс

Жизненный цикл контейнера

Контейнер может находиться в разных состояниях:

created
running
paused
exited
restarting
dead

Основные команды:

docker ps

Показывает запущенные контейнеры.

docker ps -a

Показывает все контейнеры, включая остановленные.

docker stop api

Просит основной процесс корректно завершиться.

docker start api

Запускает уже существующий остановленный контейнер.

docker restart api

Перезапускает контейнер.

docker rm api

Удаляет контейнер.

docker logs api

Показывает stdout и stderr основного процесса.

Контейнер живёт, пока работает основной процесс

Если команда внутри контейнера завершилась, контейнер останавливается.

Например:

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

Если script.py выполнился и завершился, контейнер перейдёт в exited.

Контейнер не является маленьким сервером, который должен обязательно работать вечно. Он является оболочкой вокруг процесса.


Первый запуск контейнера

Запустим Nginx:

docker run --name web -p 8080:80 nginx

Параметры:

--name web
-
имя контейнера

-p 8080:80
-
публикация порта

nginx
-
имя образа

После запуска браузер обращается к:

http://localhost:8080

А Docker передаёт трафик на порт 80 контейнера.


Порты контейнера

Контейнер имеет собственное сетевое пространство.

Если FastAPI внутри контейнера слушает порт 8000, это ещё не означает, что приложение доступно с хоста.

Порт нужно опубликовать:

docker run -p 8000:8000 fastapi-app

Формат:

host_port:container_port

Важный нюанс для FastAPI:

uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8000

Если приложение слушает только 127.0.0.1 внутри контейнера, опубликованный порт может быть недоступен извне.

0.0.0.0 означает, что процесс принимает соединения на всех сетевых интерфейсах контейнера.

EXPOSE не публикует порт

Инструкция:

EXPOSE 8000

описывает намерение образа использовать порт 8000.

Она не заменяет:

-p 8000:8000

EXPOSE является метаданными, а публикация порта выполняется при запуске.


Dockerfile

Dockerfile - текстовый файл с инструкциями сборки образа.

Простой Dockerfile для FastAPI:

FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

COPY app ./app

EXPOSE 8000

CMD [
    "uvicorn",
    "app.main:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

Сборка:

docker build -t fastapi-app .

Запуск:

docker run \
  --name fastapi-api \
  -p 8000:8000 \
  fastapi-app

Как читать Dockerfile

FROM

FROM python:3.13-slim

Задаёт базовый образ.

Образ python:3.13-slim уже содержит Python и минимальное системное окружение.

Тег 3.13-slim важен для воспроизводимости. Использование только:

FROM python

может незаметно изменить версию при следующей сборке.

Для критичных окружений образ фиксируют ещё точнее, вплоть до digest.

WORKDIR

WORKDIR /app

Задаёт рабочую директорию для следующих инструкций и команды запуска.

Если директории нет, Docker создаст её.

COPY

COPY requirements.txt .

Копирует файл из build context в образ.

Build context - директория, переданная последним аргументом команды:

docker build -t fastapi-app .

Точка означает текущую директорию.

Docker может копировать только файлы из build context.

RUN

RUN pip install -r requirements.txt

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

Результат становится частью слоя.

RUN не выполняется при каждом старте контейнера.

CMD

CMD ["uvicorn", "app.main:app", ...]

Задаёт команду по умолчанию при запуске контейнера.

Её можно переопределить:

docker run fastapi-app python -m app.worker

В одном этапе Dockerfile действует только последняя инструкция CMD.


RUN, CMD и ENTRYPOINT

Эти инструкции выполняют разные роли.

RUN

RUN pip install -r requirements.txt

Выполняется при docker build.

Используется для создания образа:

  • установки пакетов;
  • компиляции;
  • создания файлов;
  • системной настройки.

CMD

CMD ["uvicorn", "app.main:app"]

Задаёт аргументы или команду по умолчанию при docker run.

ENTRYPOINT

ENTRYPOINT ["python", "-m"]
CMD ["app.main"]

ENTRYPOINT фиксирует основную исполняемую программу, а CMD может задавать аргументы по умолчанию.

Для обычного FastAPI-контейнера одного CMD часто достаточно.

Exec form и shell form

Рекомендуемый вариант:

CMD ["uvicorn", "app.main:app"]

Это exec form.

Shell form:

CMD uvicorn app.main:app

запускает команду через shell.

Exec form лучше передаёт Unix-сигналы основному процессу, что важно для корректной остановки контейнера.


Слои и кэш сборки

Каждая инструкция Dockerfile создаёт слой или влияет на метаданные образа.

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

Такой порядок выбран специально.

Исходный код меняется часто, а зависимости реже.

Если сначала скопировать весь проект:

COPY . .
RUN pip install -r requirements.txt

любое изменение кода инвалидирует кэш шага COPY, после чего зависимости устанавливаются снова.

Более эффективная схема:

скопировать файл зависимостей
-
установить зависимости
-
скопировать исходный код

Docker сможет повторно использовать слой с зависимостями, пока их файл не изменился.

Кэш не должен влиять на корректность

Для принудительной сборки без кэша:

docker build --no-cache -t fastapi-app .

Но постоянное отключение кэша замедляет сборку. Лучше правильно организовать слои.


.dockerignore

Build context может случайно включить:

  • .git;
  • виртуальное окружение;
  • кэш Python;
  • логи;
  • тестовые данные;
  • секреты;
  • локальные базы;
  • IDE-файлы.

Файл .dockerignore исключает их:

.git
.venv
__pycache__
*.pyc
.env
.pytest_cache
.mypy_cache
dist
build

Это:

  • уменьшает контекст сборки;
  • ускоряет передачу файлов;
  • снижает риск попадания секретов в образ;
  • делает кэш стабильнее.

Важно: если секрет попал в предыдущий слой, удаление его в следующем слое не гарантирует исчезновение из истории образа.


Переменные окружения

Конфигурацию, зависящую от окружения, не стоит жёстко записывать в образ.

Пример запуска:

docker run \
  -e APP_ENV=production \
  -e DATABASE_URL=postgresql://... \
  fastapi-app

В Dockerfile можно задать значение по умолчанию:

ENV APP_ENV=production

Но секреты не следует хранить в Dockerfile:

ENV DATABASE_PASSWORD=real-password

Такое значение останется в метаданных или истории образа.

Для локальной разработки можно использовать env-файл:

docker run --env-file .env fastapi-app

Файл .env не нужно добавлять в репозиторий или образ.


Данные внутри контейнера

Контейнер получает изменяемый writable layer.

Если приложение создаёт файл:

/app/data/report.json

он существует, пока существует конкретный контейнер.

После удаления контейнера данные пропадут.

Контейнер удалён
-
его writable layer удалён

Для постоянных данных используют volumes или bind mounts.


Named volumes

Volume управляется Docker.

Создание:

docker volume create postgres_data

Запуск PostgreSQL:

docker run \
  --name db \
  -e POSTGRES_PASSWORD=secret \
  -v postgres_data:/var/lib/postgresql/data \
  postgres

Формат:

volume_name:path_inside_container

Преимущества named volume:

  • данные живут независимо от контейнера;
  • Docker управляет расположением;
  • volume можно подключить к новому контейнеру;
  • удобно для баз данных.

Удаление контейнера не удаляет volume автоматически.

Проверить:

docker volume ls

Удалить:

docker volume rm postgres_data

Bind mounts

Bind mount связывает конкретную директорию хоста с контейнером.

docker run \
  -v "$(pwd)/app:/app/app" \
  fastapi-app

Изменения локального кода сразу видны внутри контейнера.

Это удобно в разработке.

Различие:

Named volume
-
управляется Docker
-
подходит для постоянных данных сервисов

Bind mount
-
указывает на путь хоста
-
удобен для исходного кода и локальных файлов

Bind mount сильнее связывает конфигурацию с файловой системой конкретного компьютера.


Docker networks

Контейнеры не должны связываться через случайные IP-адреса.

Создадим сеть:

docker network create app_network

Запустим PostgreSQL:

docker run \
  --name db \
  --network app_network \
  -e POSTGRES_PASSWORD=secret \
  postgres

Запустим API:

docker run \
  --name api \
  --network app_network \
  -e DATABASE_HOST=db \
  fastapi-app

Внутри сети контейнер API обращается к базе по имени:

db:5432

Не по:

localhost:5432

Почему localhost не работает между контейнерами

localhost внутри контейнера указывает на этот же контейнер.

localhost внутри api - контейнер api

localhost внутри db - контейнер db

Для обращения к другому контейнеру используется его DNS-имя в общей Docker network.


Docker Compose

Если приложение состоит из API, PostgreSQL и Redis, запускать каждую команду вручную неудобно.

Docker Compose позволяет описать многоконтейнерное приложение в YAML-файле.

compose.yaml:

services:
  api:
    build:
      context: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      REDIS_HOST: redis
    depends_on:
      db:
        condition: service_healthy
    networks:
      - backend

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U app -d app
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - backend

  redis:
    image: redis:alpine
    networks:
      - backend

volumes:
  postgres_data:

networks:
  backend:

Запуск:

docker compose up

В фоне:

docker compose up -d

Остановка:

docker compose down

Удаление вместе с named volumes:

docker compose down -v

Флаг -v удаляет постоянные данные, поэтому его нужно использовать осознанно.


Dockerfile и Compose file

Они решают разные задачи.

Dockerfile

Описывает, как собрать один образ:

базовый образ
-
файлы
-
зависимости
-
команда запуска

Compose file

Описывает, как запустить набор сервисов:

какие контейнеры
-
какие образы
-
порты
-
переменные
-
сети
-
volumes
-
зависимости

Часто Compose использует Dockerfile:

services:
  api:
    build: .

Команды Docker Compose

Запустить и при необходимости создать контейнеры:

docker compose up

Пересобрать:

docker compose up --build

Посмотреть состояние:

docker compose ps

Логи:

docker compose logs

Следить за логами:

docker compose logs -f api

Выполнить команду в работающем сервисе:

docker compose exec api bash

Одноразовая команда:

docker compose run --rm api pytest

Остановить без удаления:

docker compose stop

Удалить контейнеры и сеть проекта:

docker compose down

depends_on не всегда означает готовность

Контейнер PostgreSQL может быть запущен как процесс, но база ещё не готова принимать соединения.

Простая последовательность:

контейнер db запущен
-
контейнер api запускается
-
PostgreSQL ещё инициализируется
-
API получает connection refused

Поэтому используют health check:

healthcheck:
  test:
    - CMD-SHELL
    - pg_isready -U app -d app
  interval: 5s
  timeout: 3s
  retries: 10

И условие:

depends_on:
  db:
    condition: service_healthy

Но даже с health check приложение должно уметь переживать временную недоступность зависимостей. Сеть и сервисы могут падать уже после запуска.


HEALTHCHECK

В Dockerfile:

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

Проверка возвращает:

0
-
healthy

1
-
unhealthy

Для FastAPI можно создать endpoint:

@app.get("/health")
async def health():
    return {"status": "ok"}

Но полезный health check должен отвечать конкретной задаче.

Liveness

Проверяет, жив ли процесс.

Приложение отвечает?

Readiness

Проверяет, готово ли приложение обслуживать запросы.

Подключена ли база?
Загружена ли ML-модель?
Завершена ли инициализация?

Не следует делать health check слишком тяжёлым. Он выполняется регулярно.


Multi-stage build

Иногда для сборки нужны инструменты, которые не нужны во время выполнения:

  • компилятор;
  • заголовочные файлы;
  • build dependencies;
  • Node.js для сборки frontend.

Multi-stage build использует несколько FROM.

Пример:

FROM python:3.13-slim AS builder

WORKDIR /build

COPY requirements.txt .

RUN pip wheel \
    --no-cache-dir \
    --wheel-dir /wheels \
    -r requirements.txt


FROM python:3.13-slim

WORKDIR /app

COPY --from=builder /wheels /wheels
COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    --no-index \
    --find-links=/wheels \
    -r requirements.txt \
    && rm -rf /wheels

COPY app ./app

CMD [
    "uvicorn",
    "app.main:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

Финальный образ не содержит весь набор инструментов builder-stage.

Преимущества:

  • меньший размер;
  • меньше лишних пакетов;
  • более понятное разделение сборки и запуска;
  • меньшая поверхность атаки.

Пользователь внутри контейнера

Процессы в контейнере не обязательно должны работать от root.

Пример:

RUN addgroup --system app \
    && adduser --system --ingroup app app

USER app

Полный фрагмент:

FROM python:3.13-slim

WORKDIR /app

RUN addgroup --system app \
    && adduser --system --ingroup app app

COPY --chown=app:app app ./app

USER app

CMD [
    "uvicorn",
    "app.main:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

Если приложение не требует системных привилегий, запуск от root создаёт лишний риск.

Также полезно:

  • не использовать --privileged без необходимости;
  • не монтировать Docker socket в недоверенный контейнер;
  • ограничивать capabilities;
  • делать файловую систему read-only, если возможно;
  • регулярно обновлять базовые образы.

Секреты при сборке

Нельзя передавать секрет через обычный ARG или копировать .env в образ.

Плохой пример:

ARG PRIVATE_TOKEN
RUN pip install \
    "https://${PRIVATE_TOKEN}@example.com/package"

Значение может попасть в историю сборки или кэш.

Для BuildKit существуют secret mounts, которые предоставляют секрет только конкретной инструкции сборки и не сохраняют его как слой.

Общий принцип:

Секрет нужен во время сборки
-
передать временно
-
не записывать в образ

Ограничение ресурсов

Без ограничений контейнер может использовать значительную часть памяти или CPU хоста.

Пример:

docker run \
  --memory=512m \
  --cpus=1.0 \
  fastapi-app

Ограничения полезны для:

  • защиты соседних сервисов;
  • тестирования поведения при нехватке ресурсов;
  • прогнозирования нагрузки;
  • предотвращения неконтролируемого роста памяти.

Если процесс превысит доступную память, он может быть завершён OOM killer.

Ограничения не заменяют мониторинг и профилирование.


Логи

Контейнерное приложение должно писать основные логи в:

stdout
stderr

Docker собирает этот вывод:

docker logs api

Если приложение пишет лог только в файл внутри writable layer, после удаления контейнера он исчезнет.

Для production логи обычно передают во внешнюю систему.

Полезно логировать:

  • request ID;
  • тип запроса;
  • статус;
  • время выполнения;
  • ошибки;
  • связь с бизнес-операцией.

Но не следует выводить секреты и персональные данные без необходимости.


Отладка контейнера

Посмотреть логи

docker logs -f api

Посмотреть конфигурацию

docker inspect api

Зайти внутрь контейнера

docker exec -it api sh

В slim-образе может не быть bash, поэтому используется sh.

Проверить процессы

docker top api

Проверить использование ресурсов

docker stats

Посмотреть файловую систему

docker exec api ls -la /app

Проверить переменные окружения

docker exec api env

При этом нужно осторожно работать с секретами.


Почему изменение кода не попало в контейнер

Образ является результатом конкретной сборки.

После изменения исходного кода старый образ не обновляется автоматически.

Нужно пересобрать:

docker build -t fastapi-app .

Для Compose:

docker compose up --build

Если используется bind mount для разработки, код может обновляться без пересборки. Но зависимости и системные пакеты всё равно требуют нового образа.


Контейнеризация FastAPI

Структура проекта:

project/
├── app/
│   ├── __init__.py
│   └── main.py
├── requirements.txt
├── Dockerfile
├── compose.yaml
└── .dockerignore

app/main.py:

from fastapi import FastAPI


app = FastAPI()


@app.get("/")
async def root():
    return {
        "message": "FastAPI in Docker",
    }


@app.get("/health")
async def health():
    return {
        "status": "ok",
    }

requirements.txt:

fastapi
uvicorn[standard]

Dockerfile:

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

COPY app ./app

RUN addgroup --system app \
    && adduser --system --ingroup app app \
    && chown -R app:app /app

USER app

EXPOSE 8000

CMD [
    "uvicorn",
    "app.main:app",
    "--host",
    "0.0.0.0",
    "--port",
    "8000"
]

Сборка:

docker build -t fastapi-app:1.0 .

Запуск:

docker run \
  --rm \
  -p 8000:8000 \
  fastapi-app:1.0

Документация FastAPI:

http://localhost:8000/docs

FastAPI, PostgreSQL и Compose

compose.yaml:

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: >-
        postgresql://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U app -d app
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  postgres_data:

Обратите внимание на адрес базы:

db:5432

db - имя сервиса Compose и DNS-имя внутри общей сети.

С хоста PostgreSQL не будет доступен, пока порт не опубликован:

ports:
  - "5432:5432"

Если база нужна только API, публиковать её наружу необязательно.


Теги образов

Тег помогает обозначить версию:

docker build -t my-api:1.0.0 .

Другие варианты:

my-api:1.0.1
my-api:git-a1b2c3d
my-api:staging
my-api:latest

latest не означает «самый новый по времени». Это обычный тег с особым именем.

Для воспроизводимого deployment лучше использовать неизменяемый тег или digest.

Один deployment - один конкретный образ

Не стоит перезаписывать один и тот же production-тег разным содержимым без контроля.


Registry и публикация

Назовём образ для registry:

docker tag \
  fastapi-app:1.0 \
  registry.example.com/team/fastapi-app:1.0

Авторизация:

docker login registry.example.com

Отправка:

docker push \
  registry.example.com/team/fastapi-app:1.0

На сервере:

docker pull \
  registry.example.com/team/fastapi-app:1.0

Registry позволяет отделить сборку от запуска:

CI собирает образ
-
тестирует
-
публикует в registry
-
сервер загружает готовый образ

Development и production

Окружения имеют разные требования.

Разработка

Полезны:

  • bind mounts;
  • автоматическая перезагрузка;
  • открытая документация;
  • подробные ошибки;
  • локальные порты;
  • тестовые данные.

Пример:

services:
  api:
    build: .
    command:
      - uvicorn
      - app.main:app
      - --host
      - 0.0.0.0
      - --port
      - "8000"
      - --reload
    volumes:
      - ./app:/app/app
    ports:
      - "8000:8000"

Production

Нужны:

  • неизменяемый образ;
  • отсутствие bind mount исходного кода;
  • управляемые секреты;
  • health checks;
  • ограничения ресурсов;
  • корректная остановка;
  • мониторинг;
  • фиксированная версия;
  • reverse proxy или ingress;
  • резервное копирование volumes.

Development-конфигурацию нельзя бездумно переносить в production.


Контейнеры и состояние

Хороший контейнер обычно рассматривают как заменяемый экземпляр:

остановить
-
удалить
-
создать новый из того же образа

Поэтому состояние выносят:

  • в базу данных;
  • в named volume;
  • в object storage;
  • в Redis;
  • во внешний сервис.

Если важные данные находятся только внутри writable layer контейнера, обновление может их уничтожить.


Один контейнер - один основной процесс

Это полезное правило, но не абсолютный закон.

Контейнер должен иметь один главный процесс, жизненным циклом которого управляет Docker.

Пример:

контейнер API
-
Uvicorn

контейнер worker
-
Celery worker

контейнер scheduler
-
планировщик

Так проще:

  • масштабировать компоненты отдельно;
  • читать логи;
  • проверять здоровье;
  • перезапускать;
  • задавать ресурсы.

Не стоит запускать PostgreSQL, FastAPI, Redis и Nginx внутри одного контейнера только потому, что технически это возможно.


Типичные ошибки

Путать образ и контейнер

Изменение файла внутри запущенного контейнера не меняет исходный образ.

Использовать localhost для связи между сервисами

Внутри контейнера localhost указывает на него самого.

Хранить данные базы в writable layer

При замене контейнера данные пропадут.

Копировать .env в образ

Секрет может попасть в слой и registry.

Запускать всё от root

Большинство приложений не требует root-привилегий.

Использовать только latest

Сложно понять, какая версия реально развернута.

Считать depends_on гарантией готовности

Процесс может быть запущен, но сервис ещё не принимает запросы.

Писать логи только в файл контейнера

После замены контейнера логи теряются.

Устанавливать зависимости после копирования всего проекта

Любое изменение кода ломает кэш зависимостей.

Использовать development reload в production

Reload создаёт дополнительный процесс и не предназначен для production-режима.

Запускать тяжёлые миграции при старте каждого replica

Несколько контейнеров могут одновременно попытаться изменить схему базы.


Когда Docker нужен

Docker особенно полезен, если:

  • окружение должно воспроизводиться;
  • приложение разворачивается на нескольких серверах;
  • есть несколько сервисов;
  • нужен одинаковый стек для команды;
  • используется CI/CD;
  • приложение поставляется клиенту;
  • нужно изолировать зависимости;
  • создаётся FastAPI, n8n, PostgreSQL, Redis или ML-сервис.

Когда Docker может быть лишним

  • одноразовый локальный скрипт;
  • простой учебный файл;
  • окружение уже полностью управляется платформой;
  • контейнеризация усложняет задачу без пользы;
  • команда не готова обслуживать дополнительный слой инфраструктуры.

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


Как проходит полный цикл

1. Разработчик пишет код.
2. Dockerfile описывает образ.
3. docker build создаёт image.
4. Тесты запускаются в образе.
5. Image получает версию.
6. Image отправляется в registry.
7. Сервер загружает image.
8. Из image создаётся container.
9. Порты подключаются к сети.
10. Volumes подключают постоянные данные.
11. Health checks проверяют состояние.
12. Логи и метрики отправляются наружу.
13. Новая версия заменяет старый контейнер.

Главная идея:

Собираем один раз
-
запускаем один и тот же образ
в разных окружениях

Конфигурация и секреты при этом могут отличаться, но само содержимое образа остаётся тем же.


Итог

Docker упаковывает приложение в образ и запускает его как изолированный контейнер.

Основные понятия:

Dockerfile
-
инструкции сборки

Image
-
неизменяемый шаблон

Container
-
экземпляр image

Volume
-
постоянные данные

Network
-
связь контейнеров

Compose
-
описание многоконтейнерного приложения

Registry
-
хранилище образов

Для контейнеризации FastAPI нужно:

выбрать базовый Python image
-
установить зависимости
-
скопировать код
-
запустить Uvicorn на 0.0.0.0
-
опубликовать порт

Для надёжной системы дополнительно нужны:

  • непривилегированный пользователь;
  • фиксированные версии;
  • .dockerignore;
  • health checks;
  • volumes;
  • Docker networks;
  • управление секретами;
  • ограничения ресурсов;
  • внешние логи;
  • тесты образа;
  • registry и контролируемое обновление.

Docker не заменяет архитектуру приложения. Он делает окружение, сборку и запуск явными и воспроизводимыми.

Продолжить

  1. 01 PostgreSQL: как устроена реляционная база данных Введение в PostgreSQL: сервер и клиент, таблицы, ключи, связи, SQL, ограничения, JOIN, транзакции, индексы, JSONB, роли, резервные копии и подключение из FastAPI.
  2. 02 Redis: как работает быстрое хранилище данных в памяти Введение в Redis: ключи, структуры данных, TTL, кэширование, eviction, транзакции, Pub/Sub, Streams, persistence, репликация и подключение к FastAPI.

Обратная связь

Материал был полезен?

Нашли ошибку или неточность? Сообщить →