Docker: трюки и хитрости
.dockerignore
При создании образа Docker все файлы из директории в которой находится Dockerfile копируются в контейнер. Это может привести к тому, что в контейнер попадут ненужные файлы или секреты. Чтобы предотвратить это, на том же уровне где и Dockerfile создайте файл .dockerignore, в котором укажите пути к файлам и директориям, которые следует исключить.
Начинающие специалисты очень часто забывают об этом простом и удобном способе уменьшить размер конечного образа. По своему опыту могу сказать, что обычно мои .dockerignore отличаются от .gitignore только тем что в .dockerignore имеется строка вида /.git/, то есть сказано чтобы директория с Git-индексом не попала в контейнер.
Пример простого .dockerignore:
.git node_modules *.log
Хелсчеки и контейнер autoheal
Docker позволяет использовать хелсчеки (health checks) для проверки состояния контейнера. Если контейнер работает некорректно, Docker может автоматически перезапустить его.
По умолчанию в Docker Engine отсутствует возможность автоматического перезапуска контейнеров, чтобы данную возможность включить необходимо добавить контейнер autoheal, который и будет наблюдать за другими контейнерами.
docker run -d \ --name autoheal \ --restart=always \ -e AUTOHEAL_CONTAINER_LABEL=autoheal \ -v /var/run/docker.sock:/var/run/docker.sock \ willfarrell/autoheal
Добавим пример использования хелсчеков и контейнера autoheal в файл docker-compose.yml. Для этого мы определим сервисы для нашего приложения и контейнера autoheal.
Пример docker-compose.yml: https://gist.github.com/EvilFreelancer/79841eccc471fa1c268705a6126d5af6
В этом примере мы определили сервис app с нашим приложением и добавили хелсчек для проверки его состояния. Обратите внимание на параметры healthcheck, которые указывают Docker проверять состояние контейнера с помощью curl каждую минуту. Если проверка не пройдет успешно три раза подряд, контейнер будет считаться не работающим.
Мы также добавили метку autoheal=true к сервису app, чтобы контейнер autoheal мог обнаружить его и следить за его состоянием. Сервис autoheal использует образ willfarrell/autoheal и монтирует Docker сокет для доступа к API Docker. Он периодически проверяет состояние всех контейнеров с меткой autoheal=true и перезапускает их, если они находятся в состоянии «unhealthy», то есть нездоров.
Теперь, запустив приложение с помощью docker-compose up -d, вы будете иметь автоматическое восстановление для вашего приложения, если оно станет недоступным или перестанет функционировать корректно.
Кстати, можно встроить команду хелсчека в Dockerfile, сделать это можно при помощи директивы HEALTHCHECK, например так:
FROM node:14 WORKDIR /app COPY . . RUN npm install CMD [ "npm", "start" ] HEALTHCHECK --interval=1m --timeout=3s CMD curl -f http://localhost:3000/ || exit 1
Однако, по своему опыту рекомендую описывать хелсчеки за пределами Dockerfile, в дальнейшем так будет немного проще сопровождать приложение без необходимости вносить изменения в его код.
Монтирование папок и томов
Монтирование папок и томов позволяет синхронизировать данные между хостом и контейнером. Это полезно, если вы хотите сохранить состояние контейнера (statefull режим) при его перезапуске или разделять данные между контейнерами.
К моему большому удивлению оказалось, что многие не пользуются монтированием папок, а вместо этого используют функционал томов (volumes). С одной стороны тома намного более практичные в использовании, но с другой применять их имеет смысл только в режиме Docker Swarm, когда необходимо чтобы приложение с множеством реплик имело доступ в общий том для работы с файлами.
Отличие монтирования папок от использования томов заключается в том, что при монтировании папок права доступа определяются на хосте, а при использовании томов - в самом контейнере.
Вот так можно смонтировать папку хоста:
docker run -d -p 8080:80 -v /host/data:/container/data my-image
А вот так том:
docker volume create my-volume docker run -d -p 8080:80 -v my-volume:/container/data my-image
Ещё одной любопытной особенность Docker Volumes является возможность монтировать файлы, к примеру у вас есть файл конфигурации config.ini и вы хотите положить его в папку /app.
docker run -d -p 8080:80 -v ./config.ini:/app/config.ini my-image
Но если файла ./config.ini нет, то Docker Engine создаст на его месте пустую директорию, так что будьте внимательны.
Docker Compose
Без понятия почему, но многие не пользуются Docker Compose, а ведь это удобнейший инструмент для определения и запуска многоконтейнерных Docker-приложений с использованием YAML-файла для конфигурации. Docker Compose позволяет описывать сложные приложения, состоящие из нескольких сервисов, сетей и томов, с возможностью легкого масштабирования и управления.
Пример использования переменных окружения
Docker Compose поддерживает использование переменных окружения и шаблонов в файлах docker-compose.yml. Это позволяет создавать универсальные конфигурации, которые можно настраивать с помощью переменных окружения.
Пример docker-compose.yml файла с использованием переменных окружения, которые находятся в файле .env (который находится на одном уровне с docker-compose.yml) или экспортированы в оболочку: https://gist.github.com/EvilFreelancer/45510caafdf8781c7eb50ea1788f0cf7
В этом примере используются переменные окружения для определения образов, порта и учетных данных для базы данных. Если переменная окружения не определена, будет использовано значение по умолчанию после двоеточия.
Пример использования YML-шаблонов
Это относительно новая фича схемы Docker Compose, она позволяет переиспользовать куски конфигурации в сервисах просто указывая ссылку на нужный блок настроек.
Пример docker-compose.yml с использованием YML-шаблонов: https://gist.github.com/EvilFreelancer/5af683e17dc7111ebccd92603254014d
В данном примере мы явно указываем, что необходимо добавить секцию logging используя шаблон template. Количество шаблонов неограниченно, а ещё можно использовать шаблоны внутри шаблонов.
Сеть и взаимодействие между сервисами
Docker Compose создает изолированную сеть для приложений, описанных в docker-compose.yml. Все сервисы, определенные в этом файле, работают в рамках этой сети и имеют доступ друг к другу по имени сервиса. Это облегчает настройку взаимодействия между сервисами без необходимости указывать IP-адреса и порты.
Для определения пользовательских сетей и более точной настройки взаимодействия между сервисами, можно использовать ключ networks в файле docker-compose.yml.
В примере ниже создается приложение с тремя сервисами: frontend, backend, и database. Мы создадим две сети: backend-network и frontend-network. Сервисы backend и database будут подключены к backend-network, а сервис frontend - к frontend-network. Таким образом, доступ к базе данных будет закрыт со стороны frontend.
Файл docker-compose.yml: https://gist.github.com/EvilFreelancer/18fca5fed1b0cd4c384b1235b4af6e82
В этом примере, сервис frontend не сможет напрямую подключиться к сервису database, так как они находятся в разных сетях. Однако сервис backend будет иметь доступ к обоим сетям и сможет общаться с обоими сервисами - frontend и database.
Важно отметить, что сервисы по-прежнему будут общаться друг с другом через имена сервисов, поэтому для подключения к базе данных из backend, вы можете использовать имя хоста database.
Управление процессорным временем и памятью
Docker позволяет ограничивать использование процессорного времени и памяти контейнерами. Чтобы ограничить использование ресурсов, используйте параметры –cpus, –cpu-shares, –memory и –memory-swap при запуске контейнера.
Пример ограничения ресурсов:
docker run -d --cpus=2 --memory=1g --memory-swap=2g my-image
Аналогичные настройки можно использовать и в docker-compose.yml, но есть одна небольшая проблема, начиная со схемы Docker Compose версии 2.4 данные опции можно применять только на Docker Swarm, однако, поскольку Swarm помер так и не успев родиться (Kubernetes передаёт привет) использовать его не имеет никакого смысла.
Поэтому вот пример docker-compose.yml со схемой «2.4»: https://gist.github.com/EvilFreelancer/9a2077b8613216b86e25cabe1756d137
В этом примере:
- cpus: 0.5 ограничивает сервис web на использование 50% от одного процессорного ядра.
- cpu_shares: 73 задает вес сервиса web для распределения процессорного времени относительно других контейнеров. Чем выше значение, тем больше процессорного времени будет выделено.
- cpu_quota: 50000 ограничивает сервис web на использование 50 000 микросекунд процессорного времени каждый cpu_period.
- cpu_period: 20000 задает период времени в микросекундах, в течение которого сервис web будет ограничен использованием процессорного времени в соответствии с cpu_quota.
- cpuset: «0,1» указывает, что сервис web может использовать только ядра 0 и 1.
- mem_limit: 1024m ограничивает использование памяти сервисом web до 1024 мегабайт.
- memswap_limit: 2048m ограничивает общее использование памяти, включая swap, сервисом web до 2048 мегабайт.
- mem_reservation: 512m задает минимальное количество памяти, которое должно быть доступно для сервиса web.
- cpu_count: 2 указывает, что сервис database может использовать до 2 ядер процессора.
- cpu_percent: 50 ограничивает сервис database на использование 50% от двух процессорных ядер.
А вот тут можете почитать подробности.
Оптимизация размера образов
Оптимизация размера образов является важным аспектом работы с Docker, поскольку это может сократить время скачивания, передачи и развертывания образов. Для меня всегда было удивителен тот факт, что специалисты попросту забывают данную процедуру выполнять.
Уменьшение количества слоев
Docker-образы состоят из слоев, каждый из которых формируется при выполнении инструкции в Dockerfile. Сокращение количества слоев может уменьшить размер образа и ускорить его скачивание и развертывание.
Есть несколько способов уменьшения количества слоёв, например если собрать команды установки пакетов и очистки временного кеша в один RUN то это уже позволит значительно уменьшить контейнер.
Так же стоит команды копирования в одну и ту же директорию собирать в один шаг.
Пример Dockerfile без оптимизации количества слоев:
FROM node:14 # Создание каталога приложения RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Установка зависимостей COPY package.json /usr/src/app/ COPY package-lock.json /usr/src/app/ RUN npm install RUN npm cache clean --force # Копирование исходного кода приложения COPY . /usr/src/app EXPOSE 3000 CMD [ "npm", "start" ]
Пример Dockerfile с оптимизацией количества слоев:
FROM node:14 # Создание каталога приложения и установка зависимостей WORKDIR /usr/src/app COPY package*.json ./ # Или например вот так # COPY ["./package.json", "./package-lock.json", "./"] RUN npm install && npm cache clean --force # Копирование исходного кода приложения COPY . . EXPOSE 3000 CMD [ "npm", "start" ]
Multistage build
Многоступенчатая сборка позволяет использовать несколько базовых образов и копировать файлы между стадиями сборки. Это помогает уменьшить размер конечного образа, исключая ненужные файлы и зависимости.
Пример Dockerfile с многоступенчатой сборкой для приложения на Node.js:
# Стадия - сборка FROM node:14 AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Стадия - финальный образ FROM node:14-alpine WORKDIR /app COPY --from=build /app/package*.json ./ RUN npm install --production COPY --from=build /app/dist ./dist EXPOSE 3000 CMD [ "npm", "start" ]
А вот ещё один пример для приложения на PHP:
# Стадия сборки FROM composer:latest AS composer COPY . /app WORKDIR /app RUN composer install # Стадия запуска FROM php:8.2-apache COPY --from=composer /app /var/www/html COPY ./apache-config.conf /etc/apache2/sites-enabled/000-default.conf EXPOSE 8080 RUN a2enmod rewrite
Подробнее про multistage build читайте в официальной документации Docker.
Использование шринкеров
Существует несколько инструментов для уменьшения размера Docker-образов, таких как Dive, docker-slim и других. Они анализируют образ, находят ненужные файлы, библиотеки и кэши, и удаляют их, чтобы сократить размер образа.
Обычно данную процедуру настраивают на CI/CD системах, чтобы после завершения сборки происходило «обрезание», ну или шринк.
Приложения на Go и C(++)
Приложения, написанные на языках Go и C(++), можно собирать в самодостаточные бинарные файлы, которые не требуют внешних зависимостей или среды выполнения. В таких случаях можно использовать базовый образ FROM scratch для создания минимального и небольшого образа Docker.
Пример Dockerfile для приложения на Go:
# Стадия сборки FROM golang:1.16 AS build WORKDIR /src COPY . . RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -o /app # Стадия продакшена с использованием базового образа scratch FROM scratch COPY --from=build /app /app ENTRYPOINT [ "/app" ]
Docker внутри Docker (Docker-in-Docker)
Docker-in-Docker (DinD) - это концепция запуска одного или нескольких Docker контейнеров внутри другого Docker контейнера. Это может быть полезно в определенных сценариях, таких как тестирование и настройка CI/CD пайплайнов.
Зачем использовать Docker-in-Docker
DinD может быть полезен в следующих ситуациях:
- Тестирование Docker плагинов и интеграций с другими системами.
- Изоляция Docker демона для снижения риска и избежания воздействия на хост-систему.
- Создание CI/CD пайплайнов, которые требуют работы с Docker образами и контейнерами.
Как использовать Docker-in-Docker
Чтобы запустить контейнер Docker внутри другого контейнера, выполните следующие шаги:
Запустите основной контейнер с привилегированными правами и смонтируйте Docker сокет:
docker run -it --privileged -v /var/run/docker.sock:/var/run/docker.sock docker:dind
Внутри контейнера установите Docker CLI и выполните команды Docker:
apk add docker-cli docker ps
Если вы видете списко контейнеров запущенных в хостовой системе, значит всё настроено верно.
Альтернативы Docker-in-Docker
Docker-in-Docker может иметь некоторые недостатки, такие как проблемы с безопасностью и сложность управления ресурсами. Вместо использования DinD, вы можете рассмотреть следующие альтернативы:
- Docker-outside-of-Docker** (DooD) - при использованни данного подхода вы смонтируете Docker сокет из хост-системы в контейнер. Это позволит контейнеру выполнять команды Docker, управляя контейнерами и образами на хост-системе.
- Kaniko** - это инструмент от Google, который позволяет собирать Docker образы без использования Docker демона. Это может быть полезно для CI/CD пайплайнов, где требуется только сборка образов.
- BuildKit**- это инструмент для сборки образов, разработанный Docker. Он предоставляет расширяемый и эффективный фреймворк для сборки образов без необходимости запускать Docker демон.
В зависимости от ваших потребностей и сценариев использования, вы можете выбрать подходящий вариант для работы с Docker образами и контейнерами.
Использование Docker в CI/CD
Docker может быть полезен для автоматизации процессов непрерывной интеграции (CI) и непрерывной доставки (CD). Вместе с инструментами, такими как Jenkins, GitLab CI, и GitHub Actions, вы можете автоматически собирать, тестировать и развертывать приложения на разных платформах и средах.
Очистка неиспользуемых образов и контейнеров
В процессе работы с Docker на вашей машине могут накапливаться старые образы, контейнеры и тома. Чтобы освободить дисковое пространство и улучшить производительность, регулярно удаляйте неиспользуемые ресурсы с помощью команд docker system prune, docker image prune, docker container prune и docker volume prune.
Обработка логов контейнеров
Правильная обработка и анализ логов контейнеров является важной частью работы с Docker. Это позволяет обнаруживать и решать проблемы, отслеживать состояние приложений и контейнеров, а также улучшать безопасность.
Логирование с Docker
Docker предоставляет встроенные средства для просмотра логов контейнеров. Вы можете использовать команду docker logs [container-id] для просмотра логов контейнера. Чтобы следить за логами в реальном времени, используйте флаг -f или –follow:
docker logs -f [container-id]
Драйверы логирования
Docker поддерживает различные драйверы логирования, которые позволяют настраивать, как и куда логи должны быть отправлены. Некоторые из популярных драйверов логирования включают:
- json-file (по умолчанию)
- syslog
- journald
- gelf (Graylog Extended Log Format)
- fluentd
- awslogs (Amazon CloudWatch Logs)
- splunk
Для выбора драйвера логирования используйте опцию –log-driver при запуске контейнера или укажите его в файле docker-compose.yml: https://gist.github.com/EvilFreelancer/be52bbd3187558105753d155ddc66638
Подробности можно почитать в официальной документации Docker.
Централизованное логирование
В больших системах и приложениях с множеством контейнеров централизованное логирование является ключевым аспектом для сбора и анализа логов. Это позволяет собирать логи из разных источников и агрегировать их в одном месте для анализа и мониторинга.
Популярные решения для централизованного логирования включают:
Эти решения предоставляют инструменты для сбора, обработки, индексации и визуализации логов. Они также могут интегрироваться с различными системами оповещений и предоставлять возможности поиска и анализа логов для быстрого выявления и решения проблем.
Ротация и управление логами
Для управления размером логов и предотвращения их чрезмерного роста, Docker предоставляет возможность настройки ротации логов. Это включает автоматическое удаление старых логов при достижении определенного размера или количества файлов.
Например, чтобы настроить ротацию логов для контейнера с ограничением на максимальный размер лог-файла в 10 МБ и максимальное количество лог-файлов в 3, используйте следующие опции при запуске контейнера:
docker run --log-opt max-size=10m --log-opt max-file=3 my-image
В случае использования docker-compose, укажите параметры ротации логов в файле docker-compose.yml: https://gist.github.com/EvilFreelancer/f0178d47ea3b8d64446bd92a89bedb8a
Подробности тут.
Мониторинг логов на предмет аномалий и оповещений
Централизованные системы логирования, такие как ELK Stack или Graylog, обеспечивают возможности мониторинга логов на предмет аномалий и настройки оповещений. Вы можете настроить систему таким образом, чтобы получать оповещения при возникновении определенных событий или проблем, которые могут указывать на сбои, ошибки или угрозы безопасности.
Это позволяет оперативно реагировать на проблемы, минимизировать время простоя и улучшать безопасность приложений и инфраструктуры.
Управление секретами и переменными окружения
Секреты и переменные окружения являются важными компонентами при работе с приложениями на основе Docker. В то время как переменные окружения обычно используются для настройки приложений и передачи конфигурационных параметров, секреты представляют собой чувствительные данные, такие как пароли, ключи API и сертификаты. Управление этими данными важно для обеспечения безопасности и гибкости приложений.
Переменные окружения
Переменные окружения можно передавать контейнерам во время их запуска, используя опцию -e с командой docker run:
docker run -e VARIABLE_NAME=value my-image
Для docker-compose вы можете определить переменные окружения в файле docker-compose.yml: https://gist.github.com/EvilFreelancer/ec50d3156cbbcad8516798f12e33db72
Также можно использовать файл .env для хранения переменных окружения и ссылаться на него в docker-compose.yml: https://gist.github.com/EvilFreelancer/30f7e15d39dd7f18c369e28d955cddd5
Секреты
Секреты представляют собой чувствительные данные, которые следует хранить отдельно от кода приложения и образов Docker. Docker предоставляет средства для управления секретами, особенно в Docker Swarm. Однако в стандартных контейнерах Docker управление секретами обычно происходит с использованием переменных окружения или томов.
Итог
В этой статье мы рассмотрели множество трюков и хитростей работы с Docker, которые могут быть полезными в повседневной разработке и администрировании приложений. Однако это лишь малая часть возможностей, которые предоставляет Docker, и существует множество других методов и подходов для оптимизации и улучшения работы с контейнерами.