Сети, Volume, ENV и логи в Docker
Основы: архитектурные компоненты Docker
Что такое Docker-сеть? Концепция изоляции и коммуникации
Docker-сеть — это виртуальная изолированная среда, построенная на механизмах ядра Linux (network namespaces, iptables, виртуальные Ethernet-пары), обеспечивающая коммуникацию между контейнерами и внешним миром.
Каждая сеть предоставляет контейнерам собственное сетевое пространство, включающее:
- Интерфейсы (loopback, Ethernet),
- Таблицы маршрутизации,
- Правила firewall (iptables/nftables).
Без сетей контейнеры не смогли бы взаимодействовать друг с другом или с хостом. Изоляция предотвращает конфликты портов (например, два контейнера с веб-сервером на порту 80) и обеспечивает безопасность через segmentation.
Docker Networking: bridge, host, overlay
1. Bridge (по умолчанию)
Bridge-сеть — изолированная сеть на уровне хоста, где контейнеры общаются через виртуальный коммутатор (bridge). Это аналог домашнего роутера: контейнеры получают внутренние IP-адреса, а трафик к внешнему миру проходит через NAT.
Как это работает:
- Docker Daemon создает bridge-интерфейс (обычно docker0) на хосте. Это виртуальный коммутатор уровня L2.
- Для каждого контейнера генерируется virtual Ethernet pair (veth):
- Один конец подключается к docker0 (на хосте),
- Второй — к network namespace контейнера (изолированное сетевое пространство).
- Network namespace — механизм ядра Linux, изолирующий сетевые ресурсы (интерфейсы, таблицы маршрутизации). Контейнер «видит» только свои интерфейсы, не мешая другим процессам.
- Трафик между контейнерами фильтруется через iptables:
- Правила NAT (POSTROUTING) маскируют исходящие пакеты (SNAT),
- Правила FORWARD разрешают коммуникацию внутри сети.
Пример:
- Контейнер web получает IP вида 172.17.0.2 в сети docker0.
- При запросе curl http://web с другого контейнера в той же сети:
1. DNS-резолвер Docker преобразует web в 172.17.0.2,
2. Пакет передается через veth-пару в docker0,
3. Доставляется в контейнер web без участия хоста.
Нюансы:
- Проброс портов (-p 8080:80) добавляет правило в цепочку DOCKER iptables, перенаправляющее трафик с хоста в контейнер.
- Для связи между контейнерами в разных bridge-сетях требуется user-defined bridge (создается через docker network create).
2. Host-сеть
Контейнер использует сетевой стек хоста напрямую, без изоляции. Это как запуск процесса на хосте, но с изоляцией процессов и файловой системы.
Как это работает:
- Контейнер не получает отдельный network namespace. Все сокеты хоста доступны внутри контейнера.
- Порты, занятые контейнером (например, 80), недоступны другим процессам хоста.
Пример:
- Nginx слушает порт 80 на хосте напрямую.
- Нет накладных расходов на NAT, но конфликты портов неизбежны при запуске нескольких сервисов.
Когда использовать:
- Для высокопроизводительных сетевых утилит (tcpdump, haproxy),
- В embedded-системах, где изоляция не требуется.
3. Overlay-сеть
Сеть для multi-host оркестрации (требует Docker Swarm или Kubernetes). Позволяет контейнерам на разных серверах общаться как в одной локальной сети.
Как это работает:
- Использует VXLAN (Virtual Extensible LAN) для туннелирования трафика между узлами.
- VXLAN — технология инкапсуляции L2-кадров в UDP-пакеты. Каждый пакет получает заголовок с VXLAN Network Identifier (VNI), идентифицирующим сеть.
- Распределенное хранилище (Consul/Etcd) синхронизирует состояние сети между нодами.
- Encap/Decap:
- При отправке пакета с узла A на узел B:
1. Данные инкапсулируются в UDP-дейтаграмму с VNI,
2. Передаются по физической сети,
3. На узле B извлекаются из туннеля.
Пример:
- Требует открытия портов:
- 7946 (TCP/UDP) — для обнаружения узлов,
- 4789 (UDP) — для VXLAN-трафика.
Нюансы:
- Задержки выше, чем в bridge, из-за инкапсуляции.
- В Kubernetes вместо overlay используется CNI (Container Network Interface) с плагинами (Calico, Flannel).
#Java #middle #Docker #Bridge #Host #Overlay
Основы: архитектурные компоненты Docker
Что такое Docker-сеть? Концепция изоляции и коммуникации
Docker-сеть — это виртуальная изолированная среда, построенная на механизмах ядра Linux (network namespaces, iptables, виртуальные Ethernet-пары), обеспечивающая коммуникацию между контейнерами и внешним миром.
Каждая сеть предоставляет контейнерам собственное сетевое пространство, включающее:
- Интерфейсы (loopback, Ethernet),
- Таблицы маршрутизации,
- Правила firewall (iptables/nftables).
Без сетей контейнеры не смогли бы взаимодействовать друг с другом или с хостом. Изоляция предотвращает конфликты портов (например, два контейнера с веб-сервером на порту 80) и обеспечивает безопасность через segmentation.
Docker Networking: bridge, host, overlay
1. Bridge (по умолчанию)
Bridge-сеть — изолированная сеть на уровне хоста, где контейнеры общаются через виртуальный коммутатор (bridge). Это аналог домашнего роутера: контейнеры получают внутренние IP-адреса, а трафик к внешнему миру проходит через NAT.
Как это работает:
- Docker Daemon создает bridge-интерфейс (обычно docker0) на хосте. Это виртуальный коммутатор уровня L2.
- Для каждого контейнера генерируется virtual Ethernet pair (veth):
- Один конец подключается к docker0 (на хосте),
- Второй — к network namespace контейнера (изолированное сетевое пространство).
- Network namespace — механизм ядра Linux, изолирующий сетевые ресурсы (интерфейсы, таблицы маршрутизации). Контейнер «видит» только свои интерфейсы, не мешая другим процессам.
- Трафик между контейнерами фильтруется через iptables:
- Правила NAT (POSTROUTING) маскируют исходящие пакеты (SNAT),
- Правила FORWARD разрешают коммуникацию внутри сети.
Пример:
docker run -d --name web nginx
- Контейнер web получает IP вида 172.17.0.2 в сети docker0.
- При запросе curl http://web с другого контейнера в той же сети:
1. DNS-резолвер Docker преобразует web в 172.17.0.2,
2. Пакет передается через veth-пару в docker0,
3. Доставляется в контейнер web без участия хоста.
Нюансы:
- Проброс портов (-p 8080:80) добавляет правило в цепочку DOCKER iptables, перенаправляющее трафик с хоста в контейнер.
- Для связи между контейнерами в разных bridge-сетях требуется user-defined bridge (создается через docker network create).
2. Host-сеть
Контейнер использует сетевой стек хоста напрямую, без изоляции. Это как запуск процесса на хосте, но с изоляцией процессов и файловой системы.
Как это работает:
- Контейнер не получает отдельный network namespace. Все сокеты хоста доступны внутри контейнера.
- Порты, занятые контейнером (например, 80), недоступны другим процессам хоста.
Пример:
docker run --network host -d nginx
- Nginx слушает порт 80 на хосте напрямую.
- Нет накладных расходов на NAT, но конфликты портов неизбежны при запуске нескольких сервисов.
Когда использовать:
- Для высокопроизводительных сетевых утилит (tcpdump, haproxy),
- В embedded-системах, где изоляция не требуется.
3. Overlay-сеть
Сеть для multi-host оркестрации (требует Docker Swarm или Kubernetes). Позволяет контейнерам на разных серверах общаться как в одной локальной сети.
Как это работает:
- Использует VXLAN (Virtual Extensible LAN) для туннелирования трафика между узлами.
- VXLAN — технология инкапсуляции L2-кадров в UDP-пакеты. Каждый пакет получает заголовок с VXLAN Network Identifier (VNI), идентифицирующим сеть.
- Распределенное хранилище (Consul/Etcd) синхронизирует состояние сети между нодами.
- Encap/Decap:
- При отправке пакета с узла A на узел B:
1. Данные инкапсулируются в UDP-дейтаграмму с VNI,
2. Передаются по физической сети,
3. На узле B извлекаются из туннеля.
Пример:
docker network create -d overlay my_overlay
- Требует открытия портов:
- 7946 (TCP/UDP) — для обнаружения узлов,
- 4789 (UDP) — для VXLAN-трафика.
Нюансы:
- Задержки выше, чем в bridge, из-за инкапсуляции.
- В Kubernetes вместо overlay используется CNI (Container Network Interface) с плагинами (Calico, Flannel).
#Java #middle #Docker #Bridge #Host #Overlay
👍3🔥1
Volumes и bind-mounts: управление данными
Volume — это механизм хранения данных, независимый от жизненного цикла контейнера. В отличие от данных внутри контейнера (которые удаляются при его остановке), volume сохраняет информацию даже после удаления контейнера. Это достигается через выделение отдельного места на диске хоста с управлением через Docker Engine.
Зачем это нужно?
- Персистентность данных (БД, конфиги),
- Обмен данными между контейнерами,
- Изоляция от изменений на хосте.
1. Named Volumes
Именованные тома, управляемые Docker. Хранятся в /var/lib/docker/volumes/.
Как это работает:
- Docker создает том в /var/lib/docker/volumes/db_data/_data.
- При монтировании в контейнер используется bind mount с флагом rprivate (изоляция от изменений на хосте).
- OverlayFS: Для образов с несколькими слоями volume монтируется как верхний writable-слой.
Пример:
- При старте контейнера:
1. Docker проверяет наличие тома db_data,
2. Если том новый — инициализирует его пустой директорией,
3. Монтирует в контейнер через mount-системный вызов.
Нюансы:
- Владелец тома — пользователь внутри контейнера (например, postgres в образе PostgreSQL).
- При удалении контейнера том не удаляется — требуется docker volume prune.
2. Bind Mounts
Прямое сопоставление директории хоста и контейнера.
Как это работает:
- Ядро Linux связывает inode хоста (./logs) и контейнера (/app/logs) через mount namespace.
- Все операции записи в /app/logs отражаются на хосте в ./logs.
Пример:
- inode — уникальный идентификатор файла в файловой системе. При bind mount inode хоста и контейнера совпадают.
Критические ограничения:
- Производительность ниже, чем у named volumes (отсутствие кэширования overlayfs),
- Риск повреждения данных при одновременной записи из хоста и контейнера.
#Java #middle #Docker #Volumes
Volume — это механизм хранения данных, независимый от жизненного цикла контейнера. В отличие от данных внутри контейнера (которые удаляются при его остановке), volume сохраняет информацию даже после удаления контейнера. Это достигается через выделение отдельного места на диске хоста с управлением через Docker Engine.
Зачем это нужно?
- Персистентность данных (БД, конфиги),
- Обмен данными между контейнерами,
- Изоляция от изменений на хосте.
1. Named Volumes
Именованные тома, управляемые Docker. Хранятся в /var/lib/docker/volumes/.
Как это работает:
- Docker создает том в /var/lib/docker/volumes/db_data/_data.
- При монтировании в контейнер используется bind mount с флагом rprivate (изоляция от изменений на хосте).
- OverlayFS: Для образов с несколькими слоями volume монтируется как верхний writable-слой.
Пример:
volumes:
db_data:
services:
db:
volumes:
- db_data:/var/lib/postgresql/data
- При старте контейнера:
1. Docker проверяет наличие тома db_data,
2. Если том новый — инициализирует его пустой директорией,
3. Монтирует в контейнер через mount-системный вызов.
Нюансы:
- Владелец тома — пользователь внутри контейнера (например, postgres в образе PostgreSQL).
- При удалении контейнера том не удаляется — требуется docker volume prune.
2. Bind Mounts
Прямое сопоставление директории хоста и контейнера.
Как это работает:
- Ядро Linux связывает inode хоста (./logs) и контейнера (/app/logs) через mount namespace.
- Все операции записи в /app/logs отражаются на хосте в ./logs.
Пример:
services:
app:
volumes:
- ./logs:/app/logs
- inode — уникальный идентификатор файла в файловой системе. При bind mount inode хоста и контейнера совпадают.
Критические ограничения:
- Производительность ниже, чем у named volumes (отсутствие кэширования overlayfs),
- Риск повреждения данных при одновременной записи из хоста и контейнера.
#Java #middle #Docker #Volumes
👍3🔥1
Переменные окружения (ENV, .env)
Это динамические пары ключ=значение, доступные процессу во время выполнения. В Linux они хранятся в памяти процесса как null-terminated строки (например, PATH=/usr/bin).
Как это работает в Docker:
- При старте контейнера Docker Daemon добавляет переменные в process namespace через execve-системный вызов.
- Значения копируются в память процесса (PID 1 контейнера) при его создании.
Пример:
- Внутри контейнера переменная доступна через:
Нюансы безопасности:
- Переменные видны в docker inspect и через /proc/<pid>/environ,
- Никогда не передавайте секреты через environment — используйте Docker Secrets или Vault.
Продвинутые сценарии
Секреты: от Docker Secrets до Vault
Что такое секреты в контексте Docker?
Это конфиденциальные данные (пароли, токены), требующие защищенного хранения и передачи. В отличие от переменных окружения, секреты шифруются и изолируются от неавторизованного доступа.
Docker Secrets (Swarm mode)
Как это работает:
- Секреты монтируются как файлы в /run/secrets/ через tmpfs (RAM-диск).
- При монтировании устанавливаются права 000 — доступ только через docker exec.
- Шифрование: секреты передаются по TLS между менеджерами Swarm и хранятся в Raft-логе шифрованными.
Пример:
- Raft-лог: Алгоритм консенсуса для распределенных систем. В Swarm используется для синхронизации состояния секретов.
Недостатки:
- Не подходит для Compose (требует Swarm),
- Нет ротации — при обновлении секрета нужно пересоздавать сервис.
Конфигурация логирования: драйверы и их особенности
Что такое драйвер логирования?
Это модуль, определяющий, как Docker перехватывает и обрабатывает stdout/stderr контейнера. По умолчанию используется json-file, но можно подключить сторонние системы (Fluentd, Syslog).
Как это работает:
- Docker Daemon перехватывает потоки stdout/stderr через pipe (канал межпроцессного взаимодействия).
- Данные передаются в драйвер, который форматирует и отправляет их в указанное место.
Пример для gelf:
- GELF (Graylog Extended Log Format) — бинарный формат для структурированных логов.
Поддерживает поля:
Нюансы:
- UDP не гарантирует доставку — возможна потеря логов при перегрузке,
- Для надежности используйте tcp вместо udp, но снизится производительность.
#Java #middle #Docker #env
Это динамические пары ключ=значение, доступные процессу во время выполнения. В Linux они хранятся в памяти процесса как null-terminated строки (например, PATH=/usr/bin).
Как это работает в Docker:
- При старте контейнера Docker Daemon добавляет переменные в process namespace через execve-системный вызов.
- Значения копируются в память процесса (PID 1 контейнера) при его создании.
Пример:
environment:
DB_URL: jdbc:postgresql://db:5432/mydb
- Внутри контейнера переменная доступна через:
cat /proc/1/environ | tr '\0' '\n' # PID 1 — процесс Java
Нюансы безопасности:
- Переменные видны в docker inspect и через /proc/<pid>/environ,
- Никогда не передавайте секреты через environment — используйте Docker Secrets или Vault.
Продвинутые сценарии
Секреты: от Docker Secrets до Vault
Что такое секреты в контексте Docker?
Это конфиденциальные данные (пароли, токены), требующие защищенного хранения и передачи. В отличие от переменных окружения, секреты шифруются и изолируются от неавторизованного доступа.
Docker Secrets (Swarm mode)
Как это работает:
- Секреты монтируются как файлы в /run/secrets/ через tmpfs (RAM-диск).
- При монтировании устанавливаются права 000 — доступ только через docker exec.
- Шифрование: секреты передаются по TLS между менеджерами Swarm и хранятся в Raft-логе шифрованными.
Пример:
echo "my_password" | docker secret create db_password -
- Raft-лог: Алгоритм консенсуса для распределенных систем. В Swarm используется для синхронизации состояния секретов.
Недостатки:
- Не подходит для Compose (требует Swarm),
- Нет ротации — при обновлении секрета нужно пересоздавать сервис.
Конфигурация логирования: драйверы и их особенности
Что такое драйвер логирования?
Это модуль, определяющий, как Docker перехватывает и обрабатывает stdout/stderr контейнера. По умолчанию используется json-file, но можно подключить сторонние системы (Fluentd, Syslog).
Как это работает:
- Docker Daemon перехватывает потоки stdout/stderr через pipe (канал межпроцессного взаимодействия).
- Данные передаются в драйвер, который форматирует и отправляет их в указанное место.
Пример для gelf:
logging:
driver: gelf
options:
gelf-address: "udp://graylog:12201"
- GELF (Graylog Extended Log Format) — бинарный формат для структурированных логов.
Поддерживает поля:
{"version": "1.1", "host": "app", "short_message": "Error", "level": 3}
Нюансы:
- UDP не гарантирует доставку — возможна потеря логов при перегрузке,
- Для надежности используйте tcp вместо udp, но снизится производительность.
#Java #middle #Docker #env
👍4🔥1
Тонкая настройка JVM через переменные окружения
Control Groups (cgroups) — механизм ядра Linux для ограничения, учета и изоляции использования ресурсов (CPU, память, дисковый I/O) группой процессов. Docker использует cgroups для ограничения ресурсов контейнера.
Как это влияет на JVM?
- Если в контейнере установлен лимит памяти (--memory=512m), JVM не видит его по умолчанию и пытается выделить память, превышающую лимит.
Решение:
Как это работает:
- JVM читает лимиты из /sys/fs/cgroup/memory/memory.limit_in_bytes (путь внутри cgroup контейнера).
- При старте выделяет heap через mmap с учетом лимита.
Критические ошибки:
- Если -Xmx превышает лимит cgroups, JVM упадет с Native memory allocation (mmap) failed.
- Для контейнеров никогда не используйте `-Xmx` без учета cgroups.
Пример: Java-сервис с ENV и логами в volume
docker-compose.yml
Концептуальное объяснение:
1. Сеть app_net:
- Создает bridge-сеть через docker network create.
- Контейнеры app и db получают IP в подсети 172.18.0.0/16 и могут общаться по именам (db:5432).
2. Том pg_data:
- Хранится в /var/lib/docker/volumes/pg_data/_data.
- При старте PostgreSQL инициализирует данные в этом каталоге.
3. Логирование:
- Драйвер json-file пишет логи в /var/lib/docker/containers/<id>/<id>-json.log.
- При достижении 50m файл ротируется, сохраняя 3 архива.
Код Java-приложения:
Почему это работает:
- Переменные DB_URL и DB_PASSWORD передаются через process namespace, что безопаснее hardcode.
- Том app_logs изолирует логи от жизненного цикла контейнера — данные сохраняются после docker-compose down.
- JVM учитывает лимиты cgroups благодаря -XX:MaxRAMPercentage.
Best practices
1. Сети:
- Bridge — для локальной разработки,
- Overlay/CNI — для продакшена,
- Host — только для специфических задач.
2. Volumes:
- Named volumes — для БД и персистентных данных,
- Bind mounts — только для разработки (hot-reload).
3. Секреты:
- Всегда используйте Vault или Kubernetes Secrets в продакшене,
- Избегайте .env для секретов — он не шифруется.
4. Логирование:
- Настройте централизованное логирование (Fluentd + Elasticsearch),
- Используйте gelf или fluentd вместо json-file в продакшене.
5. JVM:
- Всегда ограничивайте память через -XX:MaxRAMPercentage,
- Проверяйте лимиты cgroups через cat /sys/fs/cgroup/memory/memory.limit_in_bytes.
#Java #middle #Docker #best
Control Groups (cgroups) — механизм ядра Linux для ограничения, учета и изоляции использования ресурсов (CPU, память, дисковый I/O) группой процессов. Docker использует cgroups для ограничения ресурсов контейнера.
Как это влияет на JVM?
- Если в контейнере установлен лимит памяти (--memory=512m), JVM не видит его по умолчанию и пытается выделить память, превышающую лимит.
Решение:
environment:
JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75.0"
- -XX:MaxRAMPercentage указывает JVM использовать 75% от лимита cgroups, а не от общей памяти хоста.
Как это работает:
- JVM читает лимиты из /sys/fs/cgroup/memory/memory.limit_in_bytes (путь внутри cgroup контейнера).
- При старте выделяет heap через mmap с учетом лимита.
Критические ошибки:
- Если -Xmx превышает лимит cgroups, JVM упадет с Native memory allocation (mmap) failed.
- Для контейнеров никогда не используйте `-Xmx` без учета cgroups.
Пример: Java-сервис с ENV и логами в volume
docker-compose.yml
version: '3.8'
services:
app:
build: ./app
environment:
DB_URL: jdbc:postgresql://db:5432/mydb
DB_PASSWORD: ${DB_PASSWORD}
volumes:
- app_logs:/app/logs
logging:
driver: json-file
options:
max-size: "50m"
networks:
- app_net
db:
image: postgres:15
environment:
POSTGRES_DB: mydb
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
networks:
- app_net
volumes:
app_logs:
driver: local
pg_data:
networks:
app_net:
driver: bridge
Концептуальное объяснение:
1. Сеть app_net:
- Создает bridge-сеть через docker network create.
- Контейнеры app и db получают IP в подсети 172.18.0.0/16 и могут общаться по именам (db:5432).
2. Том pg_data:
- Хранится в /var/lib/docker/volumes/pg_data/_data.
- При старте PostgreSQL инициализирует данные в этом каталоге.
3. Логирование:
- Драйвер json-file пишет логи в /var/lib/docker/containers/<id>/<id>-json.log.
- При достижении 50m файл ротируется, сохраняя 3 архива.
Код Java-приложения:
public class Main {
public static void main(String[] args) {
String dbUrl = System.getenv("DB_URL"); // Чтение из process namespace
try (Connection conn = DriverManager.getConnection(
dbUrl, "user", System.getenv("DB_PASSWORD"))) {
// Запись в файловый лог
try (FileWriter fw = new FileWriter("/app/logs/application.log", true)) {
fw.write("DB connection established\n");
}
}
}
}
Почему это работает:
- Переменные DB_URL и DB_PASSWORD передаются через process namespace, что безопаснее hardcode.
- Том app_logs изолирует логи от жизненного цикла контейнера — данные сохраняются после docker-compose down.
- JVM учитывает лимиты cgroups благодаря -XX:MaxRAMPercentage.
Best practices
1. Сети:
- Bridge — для локальной разработки,
- Overlay/CNI — для продакшена,
- Host — только для специфических задач.
2. Volumes:
- Named volumes — для БД и персистентных данных,
- Bind mounts — только для разработки (hot-reload).
3. Секреты:
- Всегда используйте Vault или Kubernetes Secrets в продакшене,
- Избегайте .env для секретов — он не шифруется.
4. Логирование:
- Настройте централизованное логирование (Fluentd + Elasticsearch),
- Используйте gelf или fluentd вместо json-file в продакшене.
5. JVM:
- Всегда ограничивайте память через -XX:MaxRAMPercentage,
- Проверяйте лимиты cgroups через cat /sys/fs/cgroup/memory/memory.limit_in_bytes.
#Java #middle #Docker #best
👍3🔥1
Production практики для Java в Docker
Управление сборкой и архитектурой
Build args (ARG vs ENV): передача параметров при сборке
- ARG — переменная, доступная только на этапе сборки образа. Не сохраняется в финальном образе.
- ENV — переменная, доступная во время выполнения контейнера. Сохраняется в образе.
Как это работает:
- При выполнении docker build --build-arg JAVA_VERSION=17 . значение JAVA_VERSION передается в Dockerfile через ARG.
- Если ARG используется в ENV, его значение копируется в образ:
Нюансы безопасности:
- Никогда не передавайте секреты через ARG без флага --secret (например, токены для приватных репозиториев):
В Dockerfile:
- Почему это важно: Секреты, переданные через ARG, попадают в историю образа (видны в docker history).
Пример для Maven:
- MAVEN_VERSION используется только при загрузке Maven и не сохраняется в образе.
Multi-arch образы (ARM64 vs x86) и кросс-сборка
Образы, собранные для разных архитектур процессоров (x86_64, ARM64), чтобы запускаться на любом устройстве — от серверов до Raspberry Pi.
Как это работает:
- Docker использует QEMU для эмуляции архитектур через binfmt_misc (механизм ядра Linux).
- Buildx — расширение Docker CLI для кросс-платформенной сборки:
- При пушинге в реестр создается manifest list — файл, описывающий образы для разных архитектур.
Нюансы:
- Эмуляция через QEMU:
- Требует установки qemu-user-static:
- Замедляет сборку на 20-30% из-за эмуляции.
- Сборка нативно:
- Для ARM64 используйте серверы с ARM-процессорами (AWS Graviton, Raspberry Pi).
- В CI/CD настройте параллельную сборку для каждой архитектуры.
Пример для Java:
- Переменная TARGETARCH автоматически устанавливается Buildx (значения: amd64, arm64).
Использование .dockerignore для ускорения сборки
Файл, исключающий ненужные директории/файлы из build context — набора данных, передаваемых Docker Daemon при сборке.
Почему это критично:
- Build context отправляется в память Docker Daemon через сокет.
- Если в контексте есть target/ (500 МБ), сборка замедлится из-за передачи данных.
Типичное содержимое .dockerignore:
Как это работает:
1. При docker build . CLI сканирует текущую директорию.
2. Файлы из .dockerignore исключаются из контекста.
3. Оставшиеся данные архивируются и отправляются в память Docker Daemon (через /var/run/docker.sock).
Нюанс:
- .dockerignore не влияет на COPY в Dockerfile. Если файл исключен из контекста, COPY завершится ошибкой.
#Java #middle #Docker #Production
Управление сборкой и архитектурой
Build args (ARG vs ENV): передача параметров при сборке
- ARG — переменная, доступная только на этапе сборки образа. Не сохраняется в финальном образе.
- ENV — переменная, доступная во время выполнения контейнера. Сохраняется в образе.
Как это работает:
- При выполнении docker build --build-arg JAVA_VERSION=17 . значение JAVA_VERSION передается в Dockerfile через ARG.
- Если ARG используется в ENV, его значение копируется в образ:
ARG JAVA_VERSION=17
ENV JAVA_VERSION=${JAVA_VERSION}
Здесь JAVA_VERSION станет частью образа и будет доступна в runtime.
Нюансы безопасности:
- Никогда не передавайте секреты через ARG без флага --secret (например, токены для приватных репозиториев):
docker build --secret id=github,src=./github.token .
В Dockerfile:
RUN --mount=type=secret,id=github \
git clone https://$(cat /run/secrets/github)@github.com/private/repo.git
- Почему это важно: Секреты, переданные через ARG, попадают в историю образа (видны в docker history).
Пример для Maven:
ARG MAVEN_VERSION=3.8.6
ARG USER_HOME_DIR="/root"
RUN mkdir -p /usr/share/maven && \
curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
| tar -xzC /usr/share/maven --strip-components=1
- MAVEN_VERSION используется только при загрузке Maven и не сохраняется в образе.
Multi-arch образы (ARM64 vs x86) и кросс-сборка
Образы, собранные для разных архитектур процессоров (x86_64, ARM64), чтобы запускаться на любом устройстве — от серверов до Raspberry Pi.
Как это работает:
- Docker использует QEMU для эмуляции архитектур через binfmt_misc (механизм ядра Linux).
- Buildx — расширение Docker CLI для кросс-платформенной сборки:
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
- При пушинге в реестр создается manifest list — файл, описывающий образы для разных архитектур.
Нюансы:
- Эмуляция через QEMU:
- Требует установки qemu-user-static:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- Замедляет сборку на 20-30% из-за эмуляции.
- Сборка нативно:
- Для ARM64 используйте серверы с ARM-процессорами (AWS Graviton, Raspberry Pi).
- В CI/CD настройте параллельную сборку для каждой архитектуры.
Пример для Java:
# Сборка под ARM64 и x86_64
FROM --platform=$BUILDPLATFORM openjdk:17 AS builder
ARG TARGETARCH
RUN echo "Собираем под архитектуру: ${TARGETARCH}"
COPY . .
RUN ./mvnw package -DskipTests
- Переменная TARGETARCH автоматически устанавливается Buildx (значения: amd64, arm64).
Использование .dockerignore для ускорения сборки
Файл, исключающий ненужные директории/файлы из build context — набора данных, передаваемых Docker Daemon при сборке.
Почему это критично:
- Build context отправляется в память Docker Daemon через сокет.
- Если в контексте есть target/ (500 МБ), сборка замедлится из-за передачи данных.
Типичное содержимое .dockerignore:
.git
.gitignore
**/target/
**/*.log
**/.idea/
**/.vscode/
docker-compose.yml
Как это работает:
1. При docker build . CLI сканирует текущую директорию.
2. Файлы из .dockerignore исключаются из контекста.
3. Оставшиеся данные архивируются и отправляются в память Docker Daemon (через /var/run/docker.sock).
Нюанс:
- .dockerignore не влияет на COPY в Dockerfile. Если файл исключен из контекста, COPY завершится ошибкой.
#Java #middle #Docker #Production
👍4
Оптимизация образов: безопасность и размер
jlink: минимальная JVM под конкретное приложение
Инструмент JDK для создания настраиваемой JVM, включающей только необходимые модули (например, java.base, java.logging).
Как это работает:
1. Анализ зависимостей через jdeps:
2. Сборка минимальной JVM:
Результат:
- Стандартный образ OpenJDK 17 — 450 МБ.
- Образ с jlink — 120 МБ.
Нюансы:
- Некоторые библиотеки (например, Hibernate) требуют модуля jdk.unsupported.
- Проверяйте совместимость через --dry-run.
Distroless runtime образы (Google)
Образы без операционной системы, содержащие только JVM и ваше приложение. Созданы Google для минимизации attack surface.
Пример:
Как это работает:
- Distroless использует scratch (пустой образ) и добавляет только:
- Мусоросборщик (glibc или musl)
- Базовые сертификаты SSL,
- Минимальный набор библиотек для JVM
- Нет shell (/bin/sh), поэтому docker exec -it bash невозможен.
Преимущества:
- Размер образа — 80 МБ против 450 МБ у OpenJDK.
- Нулевой attack surface: нет curl, bash, apt для эксплуатации.
Отладка:
- Для диагностики используйте sidecar-контейнеры с curl или netcat.
- Логи читайте через docker logs, метрики — через /actuator/prometheus.
Non-root user: запуск Java-приложений безопасно
Запуск процесса Java не от root, чтобы ограничить ущерб при компрометации контейнера.
Как это работает:
Нюансы безопасности:
- UID/GID:
- Используйте фиксированный UID (1001), чтобы избежать конфликтов с томами.
- Если том смонтирован с правами root, пользователь 1001 не сможет писать в него.
- Capabilities:
- По умолчанию контейнер получает capabilities NET_BIND_SERVICE (привязка к портам < 1024).
- Для запуска на порту 80:
Критическая ошибка:
- Если приложение требует записи в /tmp, убедитесь, что директория доступна для записи:
#Java #middle #Docker #Debug
jlink: минимальная JVM под конкретное приложение
Инструмент JDK для создания настраиваемой JVM, включающей только необходимые модули (например, java.base, java.logging).
Как это работает:
1. Анализ зависимостей через jdeps:
jdeps --print-module-deps target/myapp.jar
Вывод: java.base,java.logging,java.xml.
2. Сборка минимальной JVM:
RUN jlink \
--add-modules java.base,java.logging,java.xml \
--output /jlinked \
--strip-debug \ — удаление отладочной информации.
--compress 2 \ — максимальное сжатие (уменьшает размер на 30%).
--no-header-files \
--no-man-pages
Результат:
- Стандартный образ OpenJDK 17 — 450 МБ.
- Образ с jlink — 120 МБ.
Нюансы:
- Некоторые библиотеки (например, Hibernate) требуют модуля jdk.unsupported.
- Проверяйте совместимость через --dry-run.
Distroless runtime образы (Google)
Образы без операционной системы, содержащие только JVM и ваше приложение. Созданы Google для минимизации attack surface.
Пример:
FROM gcr.io/distroless/java17-debian11
COPY --from=builder /app/target/myapp.jar /app.jar
ENTRYPOINT ["/app.jar"]
Как это работает:
- Distroless использует scratch (пустой образ) и добавляет только:
- Мусоросборщик (glibc или musl)
- Базовые сертификаты SSL,
- Минимальный набор библиотек для JVM
- Нет shell (/bin/sh), поэтому docker exec -it bash невозможен.
Преимущества:
- Размер образа — 80 МБ против 450 МБ у OpenJDK.
- Нулевой attack surface: нет curl, bash, apt для эксплуатации.
Отладка:
- Для диагностики используйте sidecar-контейнеры с curl или netcat.
- Логи читайте через docker logs, метрики — через /actuator/prometheus.
Non-root user: запуск Java-приложений безопасно
Запуск процесса Java не от root, чтобы ограничить ущерб при компрометации контейнера.
Как это работает:
# Создаем пользователя с UID 1001
RUN adduser --uid 1001 --disabled-password --gecos "" appuser
USER 1001
COPY --chown=1001:1001 target/myapp.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Нюансы безопасности:
- UID/GID:
- Используйте фиксированный UID (1001), чтобы избежать конфликтов с томами.
- Если том смонтирован с правами root, пользователь 1001 не сможет писать в него.
- Capabilities:
- По умолчанию контейнер получает capabilities NET_BIND_SERVICE (привязка к портам < 1024).
- Для запуска на порту 80:
# docker-compose.yml
app:
cap_add:
- NET_BIND_SERVICE
Критическая ошибка:
- Если приложение требует записи в /tmp, убедитесь, что директория доступна для записи:
RUN mkdir /app/tmp && chown 1001:1001 /app/tmp
ENV JAVA_OPTS="-Djava.io.tmpdir=/app/tmp"
#Java #middle #Docker #Debug
👍3
Минимизация attack surface (удаление ненужных слоёв)
Техники:
1. Многоступенчатая сборка (multi-stage build):
Промежуточные слои (Maven, исходники) не попадают в финальный образ.
2. Очистка кэша в том же слое:
- Если очистка в отдельном слое, размер образа не уменьшится (слой сохранится в истории).
3. Удаление ненужных файлов:
- Некоторые модули (CORBA, RMI) не используются в современных приложениях.
Performance tuning: настройка под контейнеры
CPU/Memory ограничения (--cpus, --memory)
Как это работает:
- Docker использует cgroups v2 для ограничения ресурсов:
- -m 512m — лимит памяти (записывается в /sys/fs/cgroup/memory.max),
- --cpus=1.5 — лимит CPU (эквивалентно --cpu-period=100000 --cpu-quota=150000).
Нюансы для JVM:
- По умолчанию JVM не видит лимиты cgroups и выделяет память, равную объему хоста.
- Решение:
Проверка лимитов:
Thread-pool tuning под контейнеры
Проблема:
- Стандартные пулы потоков (например, ForkJoinPool) используют Runtime.getRuntime().availableProcessors(),
- В контейнере это значение равно количеству CPU хоста, а не лимиту (--cpus).
Решение:
1. Для Spring Boot:
2. Для ванильного Java:
Как получить лимит CPU:
JVM-параметры для контейнеров
Критические параметры:
Объяснение:
- -XX:+UseContainerSupport (включено по умолчанию в JDK 8u191+):
- Позволяет JVM читать лимиты из cgroups.
- Без него -XX:MaxRAMPercentage игнорируется.
- -XX:MaxRAMPercentage=75.0:
- Heap = 75% от лимита памяти контейнера.
- Оставшиеся 25% — для Metaspace, native-памяти, стеков потоков.
- -XX:+UseZGC:
- Низколатентный GC (макс. задержки ~1 мс).
- Альтернативы: Shenandoah (JDK 12+), G1GC (по умолчанию).
- -Djava.security.egd=file:/dev/./urandom:
- Ускоряет старт приложений, заменяя блокирующий /dev/random на неблокирующий /dev/urandom.
Проверка работы GC:
- Столбец ZGC покажет использование памяти и паузы сборки мусора.
#Java #middle #Docker #Debug
Техники:
1. Многоступенчатая сборка (multi-stage build):
# Stage 1: Сборка
FROM maven:3.8.6 AS builder
COPY . .
RUN mvn package
# Stage 2: Финальный образ
FROM openjdk:17-jre-slim
COPY --from=builder target/myapp.jar /app.jar
Промежуточные слои (Maven, исходники) не попадают в финальный образ.
2. Очистка кэша в том же слое:
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
- Если очистка в отдельном слое, размер образа не уменьшится (слой сохранится в истории).
3. Удаление ненужных файлов:
RUN rm -rf $JAVA_HOME/jmods/java.corba.jmod
- Некоторые модули (CORBA, RMI) не используются в современных приложениях.
Performance tuning: настройка под контейнеры
CPU/Memory ограничения (--cpus, --memory)
Как это работает:
- Docker использует cgroups v2 для ограничения ресурсов:
docker run -m 512m --cpus=1.5 myapp
- -m 512m — лимит памяти (записывается в /sys/fs/cgroup/memory.max),
- --cpus=1.5 — лимит CPU (эквивалентно --cpu-period=100000 --cpu-quota=150000).
Нюансы для JVM:
- По умолчанию JVM не видит лимиты cgroups и выделяет память, равную объему хоста.
- Решение:
environment:
JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75.0" //указывает JVM использовать 75% от лимита cgroups.
Проверка лимитов:
docker exec myapp cat /sys/fs/cgroup/memory.max
docker exec myapp cat /sys/fs/cgroup/cpu.max
Thread-pool tuning под контейнеры
Проблема:
- Стандартные пулы потоков (например, ForkJoinPool) используют Runtime.getRuntime().availableProcessors(),
- В контейнере это значение равно количеству CPU хоста, а не лимиту (--cpus).
Решение:
1. Для Spring Boot:
# application.properties
spring.task.execution.pool.core-size=4
spring.task.execution.pool.max-size=8
2. Для ванильного Java:
int availableCpus = (int) Math.min(
Runtime.getRuntime().availableProcessors(),
Double.parseDouble(System.getenv("CPU_LIMIT"))
);
ExecutorService executor = Executors.newFixedThreadPool(availableCpus * 2);
Как получить лимит CPU:
public static int getContainerCpuLimit() {
try (BufferedReader reader = new BufferedReader(
new FileReader("/sys/fs/cgroup/cpu.max"))) {
String line = reader.readLine();
String[] parts = line.split(" ");
long quota = Long.parseLong(parts[0]);
long period = Long.parseLong(parts[1]);
return (int) (quota / period); // Например, 1.5 → 1
}
}
JVM-параметры для контейнеров
Критические параметры:
environment:
JAVA_TOOL_OPTIONS: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:+UseZGC
-Djava.security.egd=file:/dev/./urandom
Объяснение:
- -XX:+UseContainerSupport (включено по умолчанию в JDK 8u191+):
- Позволяет JVM читать лимиты из cgroups.
- Без него -XX:MaxRAMPercentage игнорируется.
- -XX:MaxRAMPercentage=75.0:
- Heap = 75% от лимита памяти контейнера.
- Оставшиеся 25% — для Metaspace, native-памяти, стеков потоков.
- -XX:+UseZGC:
- Низколатентный GC (макс. задержки ~1 мс).
- Альтернативы: Shenandoah (JDK 12+), G1GC (по умолчанию).
- -Djava.security.egd=file:/dev/./urandom:
- Ускоряет старт приложений, заменяя блокирующий /dev/random на неблокирующий /dev/urandom.
Проверка работы GC:
docker exec myapp jstat -gcutil 1 1000
- Столбец ZGC покажет использование памяти и паузы сборки мусора.
#Java #middle #Docker #Debug
👍4
Debug и мониторинг: production-ready диагностика
docker exec, attach к процессу
Стандартные команды:
Нюансы:
- В distroless-образах нет jstack/jstat.
Решение:
- Используйте sidecar-контейнер с OpenJDK:
- Или добавьте инструменты в образ через jlink:
JMX/JFR в контейнере
Настройка JMX:
Проблемы:
- RMI-порт: JMX использует динамические порты для RMI. Чтобы зафиксировать их:
- DNS-имя:
-Djava.rmi.server.hostname=myapp — имя сервиса в Docker Compose.
JFR (Java Flight Recorder):
Интеграция с Prometheus/Grafana
Шаги:
1. Добавьте Micrometer в приложение:
2. Настройте эндпоинт:
3. Docker Compose:
4. prometheus.yml:
Ключевые метрики:
- jvm_memory_used_bytes — использование heap,
- http_server_requests_seconds_count — количество HTTP-запросов,
- jvm_gc_pause_seconds — длительность GC-пауз.
#Java #middle #Docker #Debug
docker exec, attach к процессу
Стандартные команды:
# Стэк-трейс
docker exec myapp jstack 1 > thread_dump.txt
# Мониторинг GC
docker exec myapp jstat -gcutil 1 1000
# Heap-дамп
docker exec myapp jcmd 1 GC.heap_dump /tmp/heap.hprof
Нюансы:
- В distroless-образах нет jstack/jstat.
Решение:
- Используйте sidecar-контейнер с OpenJDK:
debug-tools:
image: openjdk:17
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: ["jstack", "myapp"]
- Или добавьте инструменты в образ через jlink:
RUN jlink \
--add-modules jdk.jfr,jdk.management \
--output /jlinked
JMX/JFR в контейнере
Настройка JMX:
environment:
JAVA_TOOL_OPTIONS: >-
-Dcom.sun.management.jmxremote.port=9090
-Dcom.sun.management.jmxremote.rmi.port=9090
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=myapp
ports:
- "9090:9090"
Проблемы:
- RMI-порт: JMX использует динамические порты для RMI. Чтобы зафиксировать их:
-Dcom.sun.management.jmxremote.rmi.port=9090
- DNS-имя:
-Djava.rmi.server.hostname=myapp — имя сервиса в Docker Compose.
JFR (Java Flight Recorder):
# Запуск записи
docker exec myapp jcmd 1 JFR.start name=recording duration=60s filename=/tmp/recording.jfr
# Экспорт метрик в Prometheus
RUN jcmd 1 JFR.configure stackdepth=128
Интеграция с Prometheus/Grafana
Шаги:
1. Добавьте Micrometer в приложение:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
2. Настройте эндпоинт:
management.endpoints.web.exposure.include=prometheus
3. Docker Compose:
services:
app:
ports:
- "8080:8080"
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
4. prometheus.yml:
scrape_configs:
- job_name: 'java'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
Ключевые метрики:
- jvm_memory_used_bytes — использование heap,
- http_server_requests_seconds_count — количество HTTP-запросов,
- jvm_gc_pause_seconds — длительность GC-пауз.
#Java #middle #Docker #Debug
👍4
CI/CD и финальный проект: production-ready Java-приложение с Docker
Финальный проект: архитектура и реализация
Сборка Java-приложения с Kafka и PostgreSQL
Требования к системе:
- Java 17 (сборка через Maven),
- PostgreSQL 15 (для хранения данных),
- Kafka 3.5+ (в режиме KRaft, без ZooKeeper),
- Безопасность: non-root пользователь, distroless-образы,
- Production-ready: healthcheck, лимиты ресурсов, мониторинг.
Финальный Dockerfile: multi-stage, non-root, distroless (не тестировался, возможно содержит ошибки, написан для визуализации)
Объяснение
1. Multi-stage build:
- Stage 1 (builder):
- Использует официальный образ Maven для загрузки зависимостей (dependency:go-offline ускоряет сборку в CI).
- Сборка происходит в изолированном окружении — зависимости не попадут в финальный образ.
- Stage 2 (jlink):
- Создает минимальную JVM через jlink, включая только необходимые модули (анализируйте зависимости через jdeps --print-module-deps).
- --compress 2 сжимает классы (уменьшает размер на 30%).
- Stage 3 (distroless):
- Базовый образ без ОС — только JVM и приложение.
- Нет shell, curl, apt — нулевой attack surface.
2. Non-root пользователь:
- adduser --uid 1001 создает пользователя с фиксированным UID для совместимости с томами.
- Почему это важно: Если контейнер скомпрометирован, злоумышленник не получит root-доступ к хосту.
3. JVM-параметры:
- -XX:MaxRAMPercentage=75.0 — ограничивает heap 75% от лимита контейнера (оставшиеся 25% — для Metaspace и native-памяти).
- -XX:+UseZGC — низколатентный GC (макс. паузы ~1 мс).
Критические нюансы:
- В distroless нет jcmd/jstack.
Для дампов используйте:
- Проверяйте лимиты памяти через:
#Java #middle #Docker
Финальный проект: архитектура и реализация
Сборка Java-приложения с Kafka и PostgreSQL
Требования к системе:
- Java 17 (сборка через Maven),
- PostgreSQL 15 (для хранения данных),
- Kafka 3.5+ (в режиме KRaft, без ZooKeeper),
- Безопасность: non-root пользователь, distroless-образы,
- Production-ready: healthcheck, лимиты ресурсов, мониторинг.
Финальный Dockerfile: multi-stage, non-root, distroless (не тестировался, возможно содержит ошибки, написан для визуализации)
# Stage 1: Сборка с Maven
FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B # Загружаем зависимости без исходников
COPY src ./src
RUN mvn package -DskipTests
# Stage 2: Минимальная JVM через jlink
FROM openjdk:17 AS jlink
ARG TARGETARCH
RUN jlink \
--add-modules java.base,java.logging,java.xml,java.management,java.naming \
--output /jlinked \
--strip-debug \
--compress 2 \
--no-header-files \
--no-man-pages
# Stage 3: Distroless-образ
FROM gcr.io/distroless/java17-debian11
COPY --from=jlink /jlinked /jlinked
COPY --from=builder /app/target/myapp.jar /app.jar
# Создаем non-root пользователя
RUN addgroup --gid 1001 appgroup && \
adduser --uid 1001 --gid 1001 --disabled-password --gecos "" appuser
USER 1001
# Настройка JVM под контейнеры
ENV JAVA_OPTS="\
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseZGC \
-Djava.security.egd=file:/dev/./urandom"
ENTRYPOINT ["/jlinked/bin/java", "${JAVA_OPTS}", "-jar", "/app.jar"]
Объяснение
1. Multi-stage build:
- Stage 1 (builder):
- Использует официальный образ Maven для загрузки зависимостей (dependency:go-offline ускоряет сборку в CI).
- Сборка происходит в изолированном окружении — зависимости не попадут в финальный образ.
- Stage 2 (jlink):
- Создает минимальную JVM через jlink, включая только необходимые модули (анализируйте зависимости через jdeps --print-module-deps).
- --compress 2 сжимает классы (уменьшает размер на 30%).
- Stage 3 (distroless):
- Базовый образ без ОС — только JVM и приложение.
- Нет shell, curl, apt — нулевой attack surface.
2. Non-root пользователь:
- adduser --uid 1001 создает пользователя с фиксированным UID для совместимости с томами.
- Почему это важно: Если контейнер скомпрометирован, злоумышленник не получит root-доступ к хосту.
3. JVM-параметры:
- -XX:MaxRAMPercentage=75.0 — ограничивает heap 75% от лимита контейнера (оставшиеся 25% — для Metaspace и native-памяти).
- -XX:+UseZGC — низколатентный GC (макс. паузы ~1 мс).
Критические нюансы:
- В distroless нет jcmd/jstack.
Для дампов используйте:
docker cp myapp:/tmp/heap.hprof . # Если heap_dump настроен в JAVA_TOOL_OPTIONS
- Проверяйте лимиты памяти через:
docker exec myapp cat /sys/fs/cgroup/memory.max
#Java #middle #Docker
👍3
Финальный docker-compose.yaml: Java + Kafka (KRaft) + PostgreSQL (не тестировался, возможно содержит ошибки, написан для визуализации)
Ключевые решения
1. KRaft вместо ZooKeeper:
- KAFKA_CFG_PROCESS_ROLES: "broker,controller" — единый процесс для метаданных (упрощает настройку).
- Важно: KAFKA_CFG_ADVERTISED_LISTENERS должен указывать на имя сервиса (kafka), а не на localhost.
2. Healthcheck для всех сервисов:
- Для PostgreSQL: pg_isready проверяет готовность принимать подключения.
- Для Kafka: kafka-broker-api-versions.sh убеждается, что брокер принимает запросы.
- Почему это критично: depends_on без healthcheck не предотвращает race condition.
3. Лимиты ресурсов:
- deploy.resources.limits — ограничивает использование CPU/memory через cgroups.
- Без этого JVM может выделить память, превышающую лимит контейнера (падение с OutOfMemoryError).
4. Сеть:
- Все сервисы в одной сети app_net — общаются по именам (db, kafka).
- Встроенный DNS Docker резолвит имена в IP-адреса контейнеров.
#Java #middle #Docker
version: '3.8'
services:
app:
image: myapp:latest
ports:
- "8080:8080"
environment:
DB_URL: jdbc:postgresql://db:5432/mydb
DB_PASSWORD: ${DB_PASSWORD}
KAFKA_BOOTSTRAP_SERVERS: kafka:9092
depends_on:
db:
condition: service_healthy
kafka:
condition: service_healthy
networks:
- app_net
deploy:
resources:
limits:
cpus: '1.5'
memory: 512M
db:
image: postgres:15
environment:
POSTGRES_DB: mydb
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
networks:
- app_net
deploy:
resources:
limits:
memory: 256M
kafka:
image: bitnami/kafka:3.5.1
container_name: kafka
ports:
- "9092:9092"
environment:
KAFKA_CFG_NODE_ID: 0
KAFKA_CFG_PROCESS_ROLES: "broker,controller"
KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093"
KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092"
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT"
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "PLAINTEXT"
volumes:
- kafka_data:/bitnami/kafka
healthcheck:
test: ["CMD", "kafka-broker-api-versions.sh", "--bootstrap-server", "localhost:9092"]
interval: 10s
timeout: 10s
retries: 20
networks:
- app_net
deploy:
resources:
limits:
memory: 512M
volumes:
pg_data:
driver: local
kafka_data:
driver: local
networks:
app_net:
driver: bridge
Ключевые решения
1. KRaft вместо ZooKeeper:
- KAFKA_CFG_PROCESS_ROLES: "broker,controller" — единый процесс для метаданных (упрощает настройку).
- Важно: KAFKA_CFG_ADVERTISED_LISTENERS должен указывать на имя сервиса (kafka), а не на localhost.
2. Healthcheck для всех сервисов:
- Для PostgreSQL: pg_isready проверяет готовность принимать подключения.
- Для Kafka: kafka-broker-api-versions.sh убеждается, что брокер принимает запросы.
- Почему это критично: depends_on без healthcheck не предотвращает race condition.
3. Лимиты ресурсов:
- deploy.resources.limits — ограничивает использование CPU/memory через cgroups.
- Без этого JVM может выделить память, превышающую лимит контейнера (падение с OutOfMemoryError).
4. Сеть:
- Все сервисы в одной сети app_net — общаются по именам (db, kafka).
- Встроенный DNS Docker резолвит имена в IP-адреса контейнеров.
#Java #middle #Docker
👍2
CI/CD pipeline: от коммита до production
Сборка образа в GitHub Actions
Как это работает
1. Кэширование слоев:
- actions/cache сохраняет результаты сборки в /tmp/.buildx-cache.
- При следующем запуске Buildx использует кэш через cache-from, пропуская этапы с неизмененными инструкциями (например, загрузку зависимостей Maven).
- Экономия времени: Сборка с кэшем — 2 минуты вместо 10.
2. Multi-arch сборка:
- platforms: linux/amd64,linux/arm64 — собирает образы для x86 и ARM.
- Использует QEMU для эмуляции архитектур (установлен через docker/setup-qemu-action).
3. Тегирование:
- myapp:latest — для dev-окружения (не рекомендуется для production!),
- myapp:${{ github.sha }} — уникальный тег на коммит (для отката),
- При тегировании релиза (v1.0.0) — myapp:1.0.0.
Нюансы:
- Для production никогда не используйте latest — это нарушает идемпотентность.
- Вместо latest применяйте семантическое версионирование: major.minor.patch.
#Java #middle #Docker
Сборка образа в GitHub Actions
name: Build and Push Docker Image
on:
push:
branches: [ main ]
tags: [ 'v*' ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU for multi-arch
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: ${{ runner.os }}-buildx-
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: myapp:latest,myapp:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
BUILDKIT_INLINE_CACHE=1
Как это работает
1. Кэширование слоев:
- actions/cache сохраняет результаты сборки в /tmp/.buildx-cache.
- При следующем запуске Buildx использует кэш через cache-from, пропуская этапы с неизмененными инструкциями (например, загрузку зависимостей Maven).
- Экономия времени: Сборка с кэшем — 2 минуты вместо 10.
2. Multi-arch сборка:
- platforms: linux/amd64,linux/arm64 — собирает образы для x86 и ARM.
- Использует QEMU для эмуляции архитектур (установлен через docker/setup-qemu-action).
3. Тегирование:
- myapp:latest — для dev-окружения (не рекомендуется для production!),
- myapp:${{ github.sha }} — уникальный тег на коммит (для отката),
- При тегировании релиза (v1.0.0) — myapp:1.0.0.
Нюансы:
- Для production никогда не используйте latest — это нарушает идемпотентность.
- Вместо latest применяйте семантическое версионирование: major.minor.patch.
#Java #middle #Docker
👍4
Сканирование образа на уязвимости
Добавьте в пайплайн после сборки:
Как это работает:
- Trivy сканирует образ на наличие уязвимостей в:
- Базовом образе (distroless — минимум пакетов),
- Зависимостях Java (через анализ JAR-файлов).
- ignore-unfixed: true — игнорирует уязвимости без патчей (чтобы не блокировать сборку).
- severity: CRITICAL,HIGH — падает при критических уязвимостях.
Альтернатива: Snyk
Организация production-ready релиза
Политики хранения образов в registry
1. Для DockerHub/приватного registry:
- Удаляйте образы старше 30 дней (кроме tagged релизов)
- Оставляйте последние 5 образов для каждого major-версии (например, v1.*).
2. Как автоматизировать:
- В GitLab CI используйте cleanup policy для registry,
- В AWS ECR — Lifecycle Policy:
Multi-environment setup через override.yml
Структура проекта:
Пример docker-compose.prod.yml:
Запуск для production:
Ключевые отличия:
- Dev:
- Горячая перезагрузка кода через bind mounts,
- Отключенные лимиты ресурсов.
- Production:
- Фиксированные теги образов (не latest),
- Подключение к сети мониторинга (monitoring_net),
- Приватные тома на выделенном диске (/mnt/prod/pg_data).
Будущее: переход к Kubernetes/Helm
Почему Docker Compose не для production?
- Нет оркестрации на нескольких нодах,
- Отсутствует self-healing (автовосстановление упавших сервисов),
- Нет встроенного балансировщика нагрузки.
Как мигрировать:
1. Замените `docker-compose.yml` на Helm chart:
2. Настройте values.yaml:
3. Интегрируйте Kafka через Strimzi Operator:
Преимущества Kubernetes:
- Автомасштабирование (HPA),
- Сетевая изоляция через Network Policies,
- Управление секретами через kubectl create secret.
#Java #middle #Docker
Добавьте в пайплайн после сборки:
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
Как это работает:
- Trivy сканирует образ на наличие уязвимостей в:
- Базовом образе (distroless — минимум пакетов),
- Зависимостях Java (через анализ JAR-файлов).
- ignore-unfixed: true — игнорирует уязвимости без патчей (чтобы не блокировать сборку).
- severity: CRITICAL,HIGH — падает при критических уязвимостях.
Альтернатива: Snyk
- name: Snyk Container scan
uses: snyk/actions/container@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: myapp:${{ github.sha }}
args: --severity-threshold=high --fail-on=all
Организация production-ready релиза
Политики хранения образов в registry
1. Для DockerHub/приватного registry:
- Удаляйте образы старше 30 дней (кроме tagged релизов)
- Оставляйте последние 5 образов для каждого major-версии (например, v1.*).
2. Как автоматизировать:
- В GitLab CI используйте cleanup policy для registry,
- В AWS ECR — Lifecycle Policy:
{
"rules": [
{
"rulePriority": 1,
"description": "Удалять образы старше 30 дней",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 30
},
"action": { "type": "expire" }
}
]
}
Multi-environment setup через override.yml
Структура проекта:
├── docker-compose.yml # Базовая конфигурация
├── docker-compose.dev.yml # Dev-окружение
├── docker-compose.prod.yml # Production
└── .env
Пример docker-compose.prod.yml:
services:
app:
environment:
SPRING_PROFILES_ACTIVE: prod
JAVA_TOOL_OPTIONS: >-
-XX:MaxRAMPercentage=75.0
-XX:+UseZGC
deploy:
replicas: 3
update_config:
parallelism: 1
order: start-first
networks:
- monitoring_net
db:
environment:
POSTGRES_PASSWORD: ${PROD_DB_PASSWORD}
volumes:
- /mnt/prod/pg_data:/var/lib/postgresql/data
Запуск для production:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Ключевые отличия:
- Dev:
- Горячая перезагрузка кода через bind mounts,
- Отключенные лимиты ресурсов.
- Production:
- Фиксированные теги образов (не latest),
- Подключение к сети мониторинга (monitoring_net),
- Приватные тома на выделенном диске (/mnt/prod/pg_data).
Будущее: переход к Kubernetes/Helm
Почему Docker Compose не для production?
- Нет оркестрации на нескольких нодах,
- Отсутствует self-healing (автовосстановление упавших сервисов),
- Нет встроенного балансировщика нагрузки.
Как мигрировать:
1. Замените `docker-compose.yml` на Helm chart:
helm create myapp
2. Настройте values.yaml:
app:
replicaCount: 3
image:
repository: myapp
tag: "v1.0.0"
resources:
limits:
memory: 512Mi
cpu: "1.5"
3. Интегрируйте Kafka через Strimzi Operator:
# kafka.yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
spec:
kafka:
version: 3.5.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
Преимущества Kubernetes:
- Автомасштабирование (HPA),
- Сетевая изоляция через Network Policies,
- Управление секретами через kubectl create secret.
#Java #middle #Docker
👍3