Продвинутые методы работы с Dockerfile

DockerBeginner
Практиковаться сейчас

Введение

В рамках этой лабораторной работы мы углубимся в изучение Dockerfile и разберем продвинутые концепции, которые помогут вам создавать более эффективные и гибкие Docker-образы. Мы подробно рассмотрим инструкции Dockerfile, многоэтапную сборку (multi-stage builds) и использование файлов .dockerignore. Также мы изучим критически важную концепцию слоев в Docker-образах. К концу этого занятия вы получите всестороннее понимание этих методов и сможете применять их в собственных проектах.

Эта лабораторная работа ориентирована на новичков: мы подготовили подробные пояснения и разобрали моменты, которые могут вызвать затруднения. Для редактирования файлов мы будем использовать WebIDE (VS Code), что позволит создавать и изменять код прямо в браузере.

Понимание инструкций и слоев Dockerfile

Начнем с создания Dockerfile, в котором задействуем различные инструкции. Мы соберем образ для веб-приложения на Python с использованием Flask и попутно разберем, как каждая инструкция влияет на формирование слоев нашего Docker-образа.

  1. Сначала создадим новую директорию для нашего проекта. В терминале WebIDE выполните:
mkdir -p ~/project/advanced-dockerfile && cd ~/project/advanced-dockerfile

Эта команда создает папку advanced-dockerfile внутри каталога project и переходит в нее.

  1. Теперь создадим файл приложения. В проводнике WebIDE (обычно слева) нажмите правой кнопкой мыши на папку advanced-dockerfile и выберите "New File". Назовите его app.py.

  2. Откройте app.py и добавьте следующий код на Python:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from {os.environ.get('ENVIRONMENT', 'unknown')} environment!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Это простое приложение на Flask, которое возвращает приветствие с указанием среды, в которой оно запущено.

  1. Далее создадим файл requirements.txt для указания зависимостей Python. Создайте новый файл с именем requirements.txt в той же директории и добавьте в него:
Flask==2.0.1
Werkzeug==2.0.1

Здесь мы указываем точные версии Flask и Werkzeug для обеспечения совместимости.

  1. Теперь создадим сам Dockerfile. Создайте файл с именем Dockerfile (с заглавной буквы 'D') в той же директории и добавьте следующее содержимое:
## Use an official Python runtime as the base image
FROM python:3.9-slim

## Set the working directory in the container
WORKDIR /app

## Set an environment variable
ENV ENVIRONMENT=production

## Copy the requirements file into the container
COPY requirements.txt .

## Install the required packages
RUN pip install --no-cache-dir -r requirements.txt

## Copy the application code into the container
COPY app.py .

## Specify the command to run when the container starts
CMD ["python", "app.py"]

## Expose the port the app runs on
EXPOSE 5000

## Add labels for metadata
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo for advanced Dockerfile techniques"

Разберем эти инструкции и их вклад в формирование слоев образа:

  • FROM python:3.9-slim: Это всегда первая инструкция. Она определяет базовый образ. Это создает первый слой нашего образа, включающий среду выполнения Python.
  • WORKDIR /app: Устанавливает рабочую директорию для последующих команд. Сама по себе она не создает новый слой, но влияет на поведение следующих инструкций.
  • ENV ENVIRONMENT=production: Устанавливает переменную окружения. Переменные окружения не создают новые слои, а сохраняются в метаданных образа.
  • COPY requirements.txt .: Копирует файл зависимостей из хост-системы в образ. Это создает новый слой, содержащий только этот файл.
  • RUN pip install --no-cache-dir -r requirements.txt: Выполняет команду в контейнере во время сборки. Она устанавливает зависимости Python. Это создает новый слой, содержащий все установленные пакеты.
  • COPY app.py .: Копирует код приложения в образ, создавая еще один слой.
  • CMD ["python", "app.py"]: Определяет команду, которая будет запущена при старте контейнера. Слой не создается, но задается команда по умолчанию.
  • EXPOSE 5000: Это форма документации. Она сообщает Docker, что контейнер будет прослушивать этот порт, но фактически не "пробрасывает" его. Слой не создается.
  • LABEL ...: Добавляет метаданные к образу. Как и ENV, они не создают слои, а хранятся в метаданных.

Каждая инструкция RUN, COPY и ADD в Dockerfile создает новый слой. Слои — это фундаментальная концепция Docker, позволяющая эффективно хранить и передавать образы. Когда вы вносите изменения в Dockerfile и пересобираете образ, Docker повторно использует кэшированные слои, которые не изменились, что значительно ускоряет процесс сборки.

  1. Теперь, когда мы понимаем структуру Dockerfile, соберем образ. В терминале выполните:
docker build -t advanced-flask-app .

Эта команда собирает новый Docker-образ с тегом advanced-flask-app. Точка . в конце указывает Docker искать Dockerfile в текущей директории.

Вы увидите вывод каждого этапа сборки. Обратите внимание, как каждый шаг соответствует инструкции в Dockerfile, и как Docker пишет "Using cache" для шагов, которые не изменились при повторном запуске.

  1. После завершения сборки запустим контейнер на основе нашего образа:
docker run -d -p 5000:5000 --name flask-container advanced-flask-app

Разбор параметров:

  • -d запускает контейнер в фоновом режиме (detached mode).
  • -p 5000:5000 сопоставляет порт 5000 хоста с портом 5000 контейнера.
  • --name flask-container присваивает имя нашему контейнеру.
  • advanced-flask-app — имя образа, который мы используем.

Проверить работу контейнера можно командой:

docker ps
  1. Чтобы протестировать приложение, воспользуемся командой curl:
curl http://localhost:5000

Вы должны увидеть сообщение: "Hello from production environment!"

Если возникли сложности с curl, можно открыть новую вкладку браузера и перейти по адресу http://localhost:5000. Результат должен быть тем же.

В случае проблем проверьте логи контейнера:

docker logs flask-container

Это покажет сообщения об ошибках или стандартный вывод вашего Flask-приложения.

Многоэтапная сборка (Multi-stage Builds)

Теперь, когда мы освоили базовые инструкции, перейдем к более продвинутой технике: многоэтапной сборке. Она позволяет использовать несколько инструкций FROM в одном Dockerfile. Это крайне полезно для создания компактных финальных образов путем копирования только необходимых артефактов из одного этапа в другой.

Модифицируем наш Dockerfile, чтобы уменьшить размер итогового образа:

  1. В WebIDE откройте созданный ранее Dockerfile.
  2. Замените всё его содержимое следующим кодом:
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .

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

## Final stage
FROM python:3.9-slim

WORKDIR /app

## Copy only the installed packages from the builder stage
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH
ENV ENVIRONMENT=production

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

EXPOSE 5000

LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo with multi-stage build"

Разберем, что происходит в этом Dockerfile:

  1. Мы начинаем с этапа сборки builder:

    • Используем образ python:3.9-slim для минимизации размера.
    • Устанавливаем зависимости с флагом --user. Это устанавливает пакеты в домашнюю директорию пользователя.
  2. Затем идет финальный этап:

    • Мы снова начинаем с чистого образа python:3.9-slim.
    • Копируем только установленные пакеты из этапа builder, а именно из папки /root/.local.
    • Копируем код приложения.
    • Добавляем путь к локальным бинарным файлам в PATH, чтобы Python видел установленные пакеты.
    • Настраиваем остальное (ENV, CMD, EXPOSE, LABEL).

Главное преимущество: наш финальный образ не содержит инструментов сборки и кэша pip. В нем только необходимые для работы файлы, что делает его значительно легче.

  1. Соберем этот новый многоэтапный образ:
docker build -t multi-stage-flask-app .
  1. Сравним размеры двух образов:
docker images | grep flask-app
multi-stage-flask-app         latest     7bdd1be2d1fb   10 seconds ago   129MB
advanced-flask-app            latest     c59d6fa303cc   10 minutes ago   136MB

Вы увидите, что multi-stage-flask-app меньше, чем advanced-flask-app.

  1. Запустим контейнер из нового образа:
docker run -d -p 5001:5000 --name multi-stage-container multi-stage-flask-app

Мы используем порт 5001 на хосте, чтобы избежать конфликта с первым контейнером.

  1. Проверим приложение:
curl http://localhost:5001

Вы по-прежнему должны видеть приветственное сообщение.

  1. Чтобы лучше понять разницу, воспользуйтесь командой docker history:
docker history advanced-flask-app
docker history multi-stage-flask-app

Сравните вывод. Вы заметите, что в многоэтапной сборке меньше слоев, а некоторые слои имеют меньший размер.

Многоэтапная сборка — это мощный инструмент. Она позволяет использовать любые инструменты в процессе сборки, не раздувая итоговый образ. Это особенно актуально для компилируемых языков или приложений со сложным процессом подготовки.

Использование файла .dockerignore

При сборке образа Docker отправляет все файлы из текущей директории демону Docker. Если у вас есть большие файлы, не нужные для работы приложения, это замедлит сборку. Файл .dockerignore позволяет указать, какие файлы и папки следует исключить из контекста сборки.

Создадим .dockerignore и посмотрим на него в деле:

  1. В WebIDE создайте новый файл в папке advanced-dockerfile и назовите его .dockerignore.
  2. Добавьте в него следующее содержимое:
**/.git
**/.gitignore
**/__pycache__
**/*.pyc
**/*.pyo
**/*.pyd
**/.Python
**/env
**/venv
**/ENV
**/env.bak
**/venv.bak

Что означают эти шаблоны:

  • **/.git: Игнорировать папку .git и всё её содержимое в любой поддиректории.
  • **/.gitignore: Игнорировать файлы .gitignore.
  • **/__pycache__: Игнорировать кэш Python.
  • **/*.pyc, **/*.pyo, **/*.pyd: Игнорировать скомпилированные файлы Python.
  • **/env, **/venv: Игнорировать директории виртуальных окружений.

Символы ** в начале означают "в любой директории".

  1. Чтобы увидеть эффект, создадим файлы, которые мы хотим проигнорировать:
mkdir venv
touch venv/ignore_me.txt
touch .gitignore

Это типичные элементы проекта, которые не должны попадать в Docker-образ.

  1. Снова соберем образ:
docker build -t ignored-flask-app .
  1. Чтобы убедиться, что лишние файлы не попали в сборку, проверьте историю:
docker history ignored-flask-app

Вы не увидите шагов, которые копировали бы папку venv или файл .gitignore.

Файл .dockerignore — важный инструмент для поддержания чистоты образов и ускорения процесса разработки, особенно в крупных проектах.

Продвинутые инструкции Dockerfile

На заключительном этапе мы изучим дополнительные инструкции и лучшие практики, которые сделают ваши образы более безопасными и удобными в обслуживании.

  1. В WebIDE снова откройте Dockerfile.
  2. Замените содержимое на следующее:
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

## Create a non-root user
RUN useradd -m appuser

## Install curl for healthcheck
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app

## Dynamically determine Python version and site-packages path
RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && \
    SITE_PACKAGES_PATH="/home/appuser/.local/lib/python${PYTHON_VERSION}/site-packages" && \
    mkdir -p "${SITE_PACKAGES_PATH}" && \
    chown -R appuser:appuser /home/appuser/.local

## Copy site-packages and binaries using the variable
COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}"
COPY --from=builder /root/.local/bin /home/appuser/.local/bin
COPY app.py .

ENV PATH=/home/appuser/.local/bin:$PATH
ENV ENVIRONMENT=production

## Set the user to run the application
USER appuser

## Use ENTRYPOINT with CMD
ENTRYPOINT ["python"]
CMD ["app.py"]

EXPOSE 5000

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

ARG BUILD_VERSION
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="${BUILD_VERSION:-1.0}"
LABEL description="Flask app demo with advanced Dockerfile techniques"

Разберем новые концепции:

  • RUN useradd -m appuser: Создает пользователя appuser. Запуск приложений от имени пользователя без прав root — это стандарт безопасности, ограничивающий возможный ущерб при взломе приложения.
  • RUN apt-get update && apt-get install -y curl...: Устанавливает curl, необходимый для проверки состояния (HEALTHCHECK). Мы также очищаем кэш apt для уменьшения размера.
  • RUN PYTHON_VERSION=...: Динамически определяет версию Python и создает правильные пути для пакетов пользователя appuser, устанавливая нужные права доступа.
  • ENTRYPOINT ["python"] вместе с CMD ["app.py"]: ENTRYPOINT задает основную исполняемую программу, а CMD — аргументы по умолчанию. Это дает гибкость: по умолчанию запускается app.py, но пользователь может легко передать другие скрипты при запуске контейнера.
  • HEALTHCHECK: Настраивает проверку работоспособности. Docker будет периодически выполнять команду curl, чтобы убедиться, что приложение отвечает. Если команда вернет ошибку, контейнер будет помечен как "unhealthy" (нездоров).
  • ARG BUILD_VERSION: Определяет аргумент сборки. Это позволяет передавать значения в образ прямо в момент его создания.
  • LABEL version="${BUILD_VERSION:-1.0}": Использует аргумент сборки для установки метки версии. Если аргумент не передан, используется значение по умолчанию 1.0.
  1. Соберем образ, указав версию сборки:
docker build -t advanced-flask-app-v2 --build-arg BUILD_VERSION=2.0 .
  1. Проверим создание образа:
docker images | grep advanced-flask-app-v2
  1. Запустим контейнер:
docker run -d -p 5002:5000 --name advanced-container-v2 advanced-flask-app-v2
  1. Проверим статус:
docker ps | grep advanced-container-v2

Если контейнера нет в списке запущенных, проверьте все контейнеры (docker ps -a) и изучите логи: docker logs advanced-container-v2.

  1. Проверим статус здоровья (Health Status) через некоторое время:
docker inspect --format='{{.State.Health.Status}}' advanced-container-v2

Вы должны увидеть "healthy". Если статус "unhealthy", подождите 30 секунд и попробуйте снова.

  1. Убедимся, что метка версии применилась правильно:
docker inspect -f '{{.Config.Labels.version}}' advanced-container-v2

Вывод должен быть "2.0".

  1. Финальный тест приложения:
curl http://localhost:5002

Эти методы позволяют создавать профессиональные, безопасные и готовые к эксплуатации (production-ready) Docker-образы.

Резюме

В ходе этой лабораторной работы мы изучили продвинутые методы работы с Dockerfile, которые помогут вам создавать эффективные, безопасные и удобные в поддержке образы. Мы разобрали:

  1. Подробные инструкции Dockerfile и их влияние на слои: Мы узнали, как каждая команда формирует структуру образа и как оптимизация слоев ускоряет работу.
  2. Многоэтапную сборку: Мы применили этот метод для разделения среды сборки и среды выполнения, что позволило значительно уменьшить размер итогового образа.
  3. Использование файлов .dockerignore: Мы научились исключать ненужные файлы из контекста сборки.
  4. Продвинутые инструкции: Мы изучили USER, ENTRYPOINT, HEALTHCHECK и ARG для создания гибких и защищенных контейнеров.

Эти навыки позволяют:

  • Оптимизировать размер Docker-образов.
  • Повысить безопасность за счет отказа от прав root.
  • Внедрить механизмы самодиагностики контейнеров.
  • Использовать переменные сборки для гибкого версионирования.

Использование WebIDE позволило нам бесшовно редактировать код и Docker-файлы прямо в браузере, обеспечивая удобный процесс разработки.