Урок 01 · Kubernetes на практике

Docker ровно настолько, насколько нужно для Kubernetes

Что-то не запустилось или осталось непонятным? Задайте вопрос в комментариях к разбору этого урока в канале — разберёмся вместе.

Kubernetes не запускает код напрямую — он запускает контейнеры. Прежде чем оркестрировать контейнеры, стоит один раз своими руками пройти путь Docker: собрать образ, понять слои и кеш сборки, вынести данные в том. Это единственный урок, который целиком про Docker — дальше те же идеи мы встретим внутри Kubernetes, уже под другими именами.

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

Два термина, которые часто путают:

  • Образ (image) — неизменяемый шаблон: файловая система приложения плюс инструкция, что запускать. Он лежит на диске (или в registry) и ничего не делает сам по себе.
  • Контейнер (container) — запущенный экземпляр образа. Из одного образа можно запустить сколько угодно контейнеров одновременно.

Аналогия из программирования: образ — это класс, контейнер — объект этого класса. Это ровно то различие, которое дальше будет иметь значение в Kubernetes: манифест Pod'а ссылается на образ, а кластер уже сам создаёт из него запущенные экземпляры.

Источник Официальное объяснение — Docker Docs: What is an image? и What is a container?

Практика: соберите свой первый образ

Понадобится установленный Docker Desktop (или Docker Engine на Linux) — если ещё не стоит, поставьте по официальной инструкции под вашу ОС.

  1. Создайте папку hello-fastapi, а в ней файл main.py:
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/")
    def root():
        return {"message": "Привет из контейнера"}
    
    @app.get("/health")
    def health():
        return {"status": "ok"}

    /health — не для украшения: в следующих уроках именно к таким эндпоинтам k8s будет обращаться, проверяя, жив ли ваш сервис.

  2. Рядом создайте requirements.txt:
    fastapi
    uvicorn[standard]
  3. И Dockerfile (без расширения):
    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY main.py .
    
    EXPOSE 8000
    CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

    Читается построчно: взять образ python:3.12-slim → перейти в /app → поставить зависимости → скопировать код → при запуске поднять uvicorn на порту 8000.

  4. Соберите образ (точка в конце — собрать из текущей папки):
    docker build -t hello-fastapi:0.1 .
  5. Запустите контейнер, пробросив порт:
    docker run -p 8000:8000 hello-fastapi:0.1
  6. Откройте localhost:8000/docs — увидите автосгенерированную страницу FastAPI. Это и есть ваш ощутимый результат урока: код, упакованный в переносимый образ, работает одинаково у вас и на любой другой машине с Docker.
Что вы только что сделали Именно этот образ (hello-fastapi:0.1) в следующем уроке мы опишем YAML-манифестом и запустим не командой docker run, а через Kubernetes — с автоматическим перезапуском при падении и масштабированием на несколько копий.

Слои и кеш сборки: почему порядок в Dockerfile не случаен

Каждая инструкция Dockerfile создаёт отдельный слой (layer). Docker кеширует слои и переиспользует их, если ничего не поменялось с начала файла и до этой строки. Поэтому в нашем Dockerfile COPY requirements.txt и RUN pip install стоят раньше COPY main.py — зависимости меняются редко, а код приложения часто. Если бы порядок был обратным, каждое изменение в main.py заставляло бы Docker заново ставить все зависимости.

Проверьте на практике: добавьте в main.py ещё один эндпоинт и пересоберите образ под новым тегом:

docker build -t hello-fastapi:0.2 .

В выводе шаги с COPY requirements.txt и RUN pip install будут помечены CACHED — Docker не выполнял их заново, потому что эти строки Dockerfile и файл requirements.txt не изменились. Пересобралась только последняя, самая дешёвая часть.

Тома (volumes): данные переживают контейнер

Файловая система контейнера живёт ровно столько, сколько сам контейнер. Удалите контейнер — исчезнет всё, что было записано внутри. Том (volume) — способ вынести данные наружу: подключить папку с хоста внутрь контейнера, чтобы она пережила и перезапуск, и удаление.

docker run --rm -v "$(pwd)":/data alpine ls /data

Здесь -v "$(pwd)":/data монтирует текущую папку хоста как /data внутри одноразового контейнера alpine — команда покажет содержимое вашей папки изнутри контейнера. --rm удаляет контейнер сразу после завершения, но данные на хосте, разумеется, остаются.

Источник Docker Docs: Volumes
Задел на будущее Кеш слоёв и тома вы встретите в Kubernetes снова, но в кластерном масштабе: слои остаются заботой Docker, а тома превращаются в Volume/PersistentVolume. Заодно держите в голове: по умолчанию контейнеры друг друга не видят — а в Kubernetes есть Service, который находит нужные Pod'ы по имени автоматически, даже когда их становится много. Как это устроено в Docker вручную, разбирать не будем — сразу увидим версию для кластера в отдельном уроке.

Проверьте себя

Что из перечисленного правильно описывает связь образа и контейнера?

Напишите команду, которая соберёт образ с тегом myapp:1.0 из Dockerfile в текущей папке:

Почему в Dockerfile зависимости ставят до копирования кода приложения?