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 не заменяет архитектуру приложения. Он делает окружение, сборку и запуск явными и воспроизводимыми.