Контейнеры в продакшене и облаке
Образ, который работает на ноутбуке, работает и в облаке. Это главная причина, почему контейнеры любят в продакшене. Образ собирают один раз. Запускают где угодно.
Путь от кода до продакшена
Заголовок раздела «Путь от кода до продакшена»код в Git → CI собирает образ → образ в реестре → облако запускает контейнер- Вы пушите код в Git.
- CI (например, GitHub Actions) выполняет
docker build. - CI отправляет образ в реестр (
docker push). - Облачный сервис берёт образ из реестра и запускает контейнер.
Сам образ на пути не меняется. Меняются только переменные окружения: адрес базы, пароль, имя бакета.
Реестры образов
Заголовок раздела «Реестры образов»Реестр — хранилище образов. docker pull берёт образ оттуда.
| Реестр | Где |
|---|---|
| Docker Hub | Общий публичный реестр |
GHCR (ghcr.io) | GitHub. Удобен, если код уже на GitHub |
| ECR | AWS |
| Artifact Registry | Google Cloud |
| ACR | Azure |
Обычно берут реестр того облака, где работает контейнер. Так быстрее и проще с правами.
Где запускают контейнеры
Заголовок раздела «Где запускают контейнеры»| Облако | Сервис | Для чего |
|---|---|---|
| AWS | ECS на Fargate | Сервисы и задачи без управления серверами |
| AWS | AWS Batch | Большие пакетные задачи с очередью |
| AWS | Lambda (образ контейнера) | Короткие задачи по событию, до 15 минут. Образ должен быть на базе public.ecr.aws/lambda/python или содержать runtime interface client. Обычный python:3.12-slim не подойдёт |
| AWS | ECS Express Mode | Простой веб-сервис из образа. App Runner с 30.04.2026 закрыт для новых клиентов |
| Google Cloud | Cloud Run (services) | HTTP-сервис, масштабируется до нуля |
| Google Cloud | Cloud Run (jobs) | Пакетная задача: запустилась, отработала, остановилась |
| Azure | Container Apps (apps) | Сервисы и API |
| Azure | Container Apps (jobs) | Задачи по расписанию или по событию |
Для данных чаще всего нужна пакетная задача. Это контейнер, который запускается по расписанию, грузит данные и завершается. Именно так работает наш load_orders.py.
Кто запускает задачу по расписанию:
| Сервис | Расписание |
|---|---|
| ECS на Fargate | Amazon EventBridge Scheduler |
| Cloud Run jobs | Cloud Scheduler |
| Container Apps jobs | Встроенный cron в настройках задачи |
| Любой из них | Airflow, если между задачами есть зависимости |
Контейнеры и Airflow
Заголовок раздела «Контейнеры и Airflow»- DockerOperator. Задача DAG запускает контейнер на хосте. Окружение задачи не зависит от окружения Airflow.
- KubernetesPodOperator. То же самое, но контейнер запускается в Kubernetes.
- Управляемый Airflow. MWAA (AWS), Cloud Composer (Google Cloud), Astronomer. Облако само поддерживает Airflow. Вам не нужно запускать Kubernetes.
Про Kubernetes — в следующем уроке.
Правила для продакшена
Заголовок раздела «Правила для продакшена»| Правило | Что значит |
|---|---|
| Конфиг через переменные окружения | Один образ для dev и prod. Меняются только переменные |
| Секреты не в образе | Пароли и ключи берите из менеджера секретов (Secrets Manager, Secret Manager, Key Vault). Не пишите их в Dockerfile и не коммитьте .env |
| Логи в stdout и stderr | Пишите через print или logging в консоль. Облако соберёт их само |
| Неизменяемые теги | Тегируйте образ хешем коммита: orders:9554b77. Не используйте latest |
| Не root | USER appuser в Dockerfile |
| Лимиты ресурсов | --memory и --cpus. Один плохой контейнер не должен занять весь сервер |
| Health check | Оркестратор проверяет, что контейнер жив, и перезапускает его |
| Сканирование образов | Реестр или CI ищет известные уязвимости |
| Маленькие образы | -slim. Быстрее скачать, меньше уязвимостей. alpine хорош для утилит, но не для Python: многие пакеты под него не собраны |
| Один процесс на контейнер | База, приложение и планировщик — разные контейнеры |
| Контейнер без состояния | Для приложений и задач: данные живут в базе или S3, не в файлах контейнера. Саму базу держите в управляемом сервисе или в томе с бэкапами |
Лимиты ресурсов
Заголовок раздела «Лимиты ресурсов»Ограничьте память и процессор:
docker run -d --name orders-limited --memory 256m --cpus 1 alpine:3.22 sleep 300docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}' orders-limitedОтвет:
orders-limited 1.344MiB / 256MiB 0.00%После / стоит лимит: 256MiB. Если контейнер превысит память, Docker остановит его (OOM kill). Лучше узнать об этом сейчас, чем в продакшене.
Удалите контейнер: docker rm -f orders-limited.
Тег с хешем коммита
Заголовок раздела «Тег с хешем коммита»git rev-parse --short HEADОтвет — короткий хеш, например 9554b77. Тег показывает, из какого коммита собран образ. Откат — это запуск старого тега.
docker build -t orders:9554b77 .GitHub Actions: сборка и отправка в GHCR
Заголовок раздела «GitHub Actions: сборка и отправка в GHCR»Файл .github/workflows/docker.yml. Мы его не запускали. Версии действий мы проверили на день написания (ноябрь 2026). Проверьте актуальные версии на страницах действий.
name: docker
on: push: branches: [main]
permissions: contents: read packages: write
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7 with: context: . push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }}Что здесь важно:
permissions: packages: writeразрешает токенуGITHUB_TOKENписать образы в GHCR. Без негоpushвернёт ошибку доступа.- Тег — полный хеш коммита
github.sha. Это неизменяемый тег. - Пароль не написан в файле. Токен выдаёт GitHub.
- Имя образа должно быть в нижнем регистре. Если в имени репозитория есть заглавные буквы, добавьте шаг
docker/metadata-action@v6. Он сам делает имя маленькими буквами и готовит теги.
Практика
Заголовок раздела «Практика»Задание 1
Заголовок раздела «Задание 1»Создайте Git-репозиторий в ~/docker-practice и сделайте коммит. Возьмите короткий хеш и поставьте его тегом на образ alpine:3.22 с именем orders-demo.
Проверка: docker images orders-demo показывает тег, равный git rev-parse --short HEAD. У вас хеш будет другой.
Решение
cd ~/docker-practicegit init -qgit config user.email you@example.comgit config user.name yougit add .git commit -qm "init"SHA=$(git rev-parse --short HEAD)docker tag alpine:3.22 orders-demo:$SHAdocker images orders-demoУберите за собой: docker rmi orders-demo:$SHA. Это удалит только тег. Образ alpine:3.22 останется.
Задание 2
Заголовок раздела «Задание 2»Запустите alpine:3.22 с командой sleep 300, лимитом памяти 128 МБ и одним процессором. Покажите использование памяти.
Проверка: в колонке памяти после / стоит 128MiB.
Решение
docker run -d --name limit-test --memory 128m --cpus 1 alpine:3.22 sleep 300docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' limit-testdocker rm -f limit-testЗадание 3
Заголовок раздела «Задание 3»В Dockerfile есть шесть строк. Какие из них плохие для продакшена и почему?
FROM python:latestCOPY . /appENV DB_PASSWORD=secret123RUN pip install psycopgUSER rootCMD ["python", "/app/load_orders.py"]Проверка: найдите пять проблем.
Решение
| Строка | Проблема | Как исправить |
|---|---|---|
FROM python:latest | latest меняется. Образ тяжёлый | FROM python:3.12-slim |
COPY . /app | Копирует всё, даже .env и .git. Ломает кэш | Сначала requirements.txt, потом код. Добавьте .dockerignore |
ENV DB_PASSWORD=secret123 | Пароль остаётся в образе и в истории слоёв | Передавайте пароль при запуске из менеджера секретов |
RUN pip install psycopg | Нет версии. Нет --no-cache-dir. Нет requirements.txt | pip install --no-cache-dir -r requirements.txt |
USER root | Запуск от root | USER appuser |
Строка CMD в порядке. Это шестая строка, и к ней нет замечаний.
Дальше: Нужен ли Kubernetes.