The book of Magnus

IT-заметки и знания

Docker. Глава вторая

Tags = [ DevSecOps, Docker ]

Вообще второй главой должна была идти инструкция по установке Docker на различные ОС, но так как эта тема довольно скучная да и может меняться время от времени, так что я просто оставлю это здесь.

Мы же двинемся дальше и посмотрим как работать с образами Docker.

Структура Dockerfile

Для создания образа Docker с помощью инструментов по умолчанию нужен dockerfile. Типичный dockerfile может содержать следующие параметры

FROM ubuntu:22.04.3
ARG email="test@test.com"
LABEL "maintainer"=$email
USER root
ENV AP /data/app
ENV SCPATH /etc/supervisor/conf.d
RUN apt-get -y update
RUN apt-get install supervisor
RUN mkdir -p /var/log/supervisor
COPY ./supervisord/conf.d/* $SCPATH/
WORKDIR #AP 
RUN npm install
CMD ["supervisord", "-n"]

FROM указывает на базовый образ ОС, который будет взят за основу. Базовым образом обычно выбирают Alpine linux, так как он занимает очень мало места, но есть одно но. Данный образ основан на библиотеке musl, а не на стандартной GNU C. Это может повлиять на приложения на основе Java и разрешение имён DNS. Плюс используется bin/sh, вместо /bin/bash. При необходимости нужные библиотеки можно доустановить.

ARG позволяет задавать переменные, которые доступны только во время сборки образа.

LABEL добавляет метки с помощью пар "ключ - значение", которые затем можно использовать для поиска и идентификации контейнеров. Посмотреть метки образа можно с помощью команды docker image inspect.

USER позволяет менять пользователя в контейнере, так как по умолчанию Docker работает от рута.

ENV задаёт переменные командной оболочки, которые приложение может использовать для конфигурации во время работы или в процессе сборки.

RUN выполняет необходимые команды.

В данном примере указана команда apt-get -y update, но в реальном образе её использование не рекомендуется, так как образ перестанет быть воспроизводимым, так как версии пакетов в репозиториях со временем меняются.

COPY копирует файлы из локальной системы в образ.

ADD может копировать не только файлы, но и архивы. И даже из сети по ссылкам.

WORKDIR меняет рабочий каталог в образе для остальных инструкций в сборке.

CMD определяет команду, запускающую процесс, который будет выполняться в контейнере.

Порядок команд в Dockerfile влияет на время сборки. Изменения, которые происходят чаще всего должны располагаться как можно ближе к концу файла, чтобы при сборке пересобиралось как можно меньше слоёв.
Каждое использование COPY и RUN создаёт новый слой в образе, так что для ускорения сборки и уменьшения размера образа будет разумно располагать данные команды на одной строке.

Сборка образа

Рядом с файлом dockerfile можно расположить файл .dockerignore, в котором можно указать файлы и каталоги, которые не должны попасть на хост Docker при сборке образа, например, туда можно добавить .git.

При сборке Docker полагает, что dockerfile находится в текущем каталоге. Если это не так, то путь можно указать с помощью аргумента -f.

Также Docker при сборке использует кэш, но бывают случаи, когда необходимо этого не делать, тогда нужно использовать ключ --no-cache.

Сборку образа можно запустить следующими командами:

docker image build -t example/docker-hello:latest .
или
docker build -t example/docker-hello:latest .

Запуск образа

Запустить собранный образ можно командой

docker container run --rm -d -p 8080:8080 example/docker-hello:latest

где --rm указывает удалить контейнер после выполнения работы

-d запускает его в фоновом режиме

-p 8080:8080 указывает с какого порта хоста на какой порт контейнера сделать проброс.

После запуска определить IP-адрес контейнера можно, посмотрев вывод команды docker context list или при сборке задать переменную DOCKER_HOST. Также если был задан UNIX-сокет для DOCKER_ENDPOINT, то адрес будет 127.0.0.1.

Аргументы, которые мы задавали при сборке, можно переопределить при запуске с помощью аргумента --build-arg, например

docker image build --build-arg email=test2@test1.org

Также можно передавать значения переменных, в уже готовые контейнеры при запуске но с помощью аргумента --env

docker container run --env WHO="blablabla"

Хранение образов

Есть несколько мест для хранения образов - публичные реестры и частные реестры.

Самым популярным публичным является Docker Hub.

Если нужно использовать частный реестр, то можно использовать варианты, такие как:

Чтобы залогиниться в реестре нужно ввести команду docker login, потом свои логин и пароль, а Docker сохранит их и будет в будущем использовать при необходимости.

Для безопасности лучше использовать не логин и пароль, а токен доступа, который можно создать в личном кабинете

Для отправки образов в хаб необходимо использовать команду docker image build -t docker.io/USER_NAME/docker-test:latest, где docker.io - имя реестра, а USER_NAME - логин.

Если мы хоти заменить теги для созданного ранее образа, то можно использовать команду docker image tag.

Для поиска доступных образов используется команда docker search NEED_NAME.

Оптимизация образа

Для того чтобы знать что оптимизировать нужно уметь смотреть что есть внутри имеющегося контейнера. Для этого есть два способа.

Первый, можно экспортировать содержимое контейнера в архив и потом его посмотреть - docker container export CONTAINER_ID -0 test.tar.

Второй, подключиться напрямую к серверу Docker:

  1. Определяем где хранятся файлы образа - docker image inspect ubuntu:latest
  2. Подключаемся - docker container run --rm -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

Многоэтапные сборки

Суть многоэтапной сборки проста. Например, нам нужно получить контейнер в котором будет какое-нибудь приложение на go. Для этого в dockerfile мы сначала запустим один контейнер, где будет проходить вся сборка, но будет много ненужного софта для работы самого приложения, а потом перенесём полученный артефакт в другой контейнер, в котором будет минимальное окружение, а значит и минимально занимаемый объём.

Пример:

# Этап 1: Сборка приложения
FROM golang:1.22-alpine AS builder

# Устанавливаем рабочую директорию
WORKDIR /app

# Копируем go.mod и go.sum для предварительной загрузки зависимостей
COPY go.mod go.sum ./
RUN go mod download

# Копируем исходный код
COPY . .

# Компилируем приложение
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -o /myapp .

# Этап 2: Финальный образ
FROM alpine:latest

# Устанавливаем рабочую директорию
WORKDIR /

# Копируем скомпилированный бинарь из этапа сборки
COPY --from=builder /myapp .

# Указываем команду запуска
CMD ["./myapp"]

Здесь нужно обратить внимание на строки FROM golang:1.22-alpine AS builder и FROM alpine:latest. Именно в них происходит магия многоэтапной сборки.

Суммирование слоёв

В Docker слои всегда суммируются. Соответственно для уменьшения образа нужно удалять всё что не нужно для работы образа на том же слое, что и добавлять, располагая команды на одной строке. Для этого нужно помнить, что слои формируются командами FROM, RUN, COPY и ADD.

Мультиархитектурные сборки

Для сборок под различные архитектуры был разработан плагин buildx и обычно он уже установлен в системе. Проверить его наличие можно с помощью команды:

docker buildx version

Чтобы собрать образы для разных платформ нужно ввести подобную команду:

docker buidx build --platform linux/amd64, linux/arm64 --tag docker.io/USERNAME/test:latest .