Одним из распространенных инструментов для контейнеризации приложений является Docker. Docker использует механизмы ядра Linux, которые позволяют изолировать процессы, ограничивать ресурсы для них и использовать общее ядро
Этими механизмами являются пространства имен и контрольные группы:
С помощью пространств имен можно изолировать процессы друг от друга. Для этого существует 8 пространств имен:
root внутри контейнераПространства имен можно создавать с помощью утилиты unshare:
sudo unshare --uts /bin/bash
Оболочка Bash запуститься в окружении в изолированном пространстве UTS. Для создания пространств имен используются флаги --ipc, --mount, --net, --pid, --uts, --user, --cgroup, --time
Для создания контейнеров интересны три пространства: PID, Network и Mount
Чтобы создать пространство имен идентификаторов процессов, можно использовать команду:
sudo unshare --pid --mount-proc --fork /bin/bash
Без флага --fork только дети процесса /bin/bash будут иметь новое пространство, а флаг --mount-proc автоматически создает пространство имен Mount и монтирует виртуальную файловую систему /proc для корректной работы утилит ps и других
После выполнения команды процесс /bin/bash будет иметь идентификатор 1
Чтобы создать сетевое пространство имен, используется команда:
sudo unshare --net /bin/bash
Такой способ применятся, если нужно, чтобы пространство имен имело срок жизни, определенный процессом /bin/bash. Для большей персистентности используют команду:
ip netns add <имя-нового-пространства>
Далее исполняемый файл можно запустить с помощью команды:
ip netns exec <имя-нового-пространства> /bin/bash
Процессы в этом пространстве будут иметь свой список сетевых интерфейсов. Для общения процессов из пространства обычно создают виртуальный сетевой мост, который перенаправляет запросы из контейнера на существующие в операционной системе сетевые интерфейсы
Для изоляции точек монтирования используется команда:
unshare --mount /bin/bash
Процесс /bin/bash получит свою таблицу с точками монтирования
Далее для изоляции файловой системы используется команда chroot (от change root). Для этого создается директория, которая станет будущим корнем, и в нужном окружении выполняется команда:
chroot <новый-корень>
Процессы после выполнения chroot будут видеть <новый-корень> как /
Более современным способом является команда pivot_root, которая перемещает корневую файловую систему текущего процесса в другую директорию, а на её место монтирует новую. Этот системный вызов предпочтительнее для контейнеров, так как он позволяет корректно размонтировать старую корневую файловую систему и обеспечивает более строгую изоляцию
Чтобы ограничивать ресурсы процессора, ОЗУ, ПЗУ и прочего, используется механизм контрольных групп. Для создания контрольных групп есть три способа:
Использование виртуальной файловой системы /sys/fs/cgroup. Для этого нужно:
Для новой группы создать директорию:
sudo mkdir /sys/fs/cgroup/my-group
Созданная директория наполнится нужными файлами. Группы могут выстраивать иерархию с наследованием ограничений
Настроить лимиты в нужных файлах
Запустить процесс и поместить его в группу - для этого нужно записать идентификатор процесса в файл cgroup.procs:
echo $APP_PID | sudo tee /sys/fs/cgroup/my-group/cgroup.procs
Использование функций системы инициализации systemd
systemd-run --scope --pids-max=100 --memory-max=2G --unit=my-limited-app.service <команда>
Здесь указаны: максимальное число процессов в группе, максимальный объем ОЗУ, имя создаваемой службы и исполняемая команда. Указание имени позволяет управлять службой в рамках systemd
Другим способом является создание файла, который описывает службы и наложенные на нее ограничения:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
# ...
[Service]
# ...
MemoryMax=1G
CPUWeight=100
Через утилиты пакета libcgroup
Для создания группы используется команда
sudo cgcreate -g cpu,memory:/my_group
Здесь cpu,memory - это контроллеры, которые ограничивают ресурсы в контрольной группе
Далее задаются лимиты (например, ОЗУ):
sudo cgset -r memory.limit_in_bytes=500M my_group
А затем исполняется команда внутри контрольной группы:
sudo cgexec -g cpu,memory:/my_group firefox &
В Docker управлением контейнерами занимается демон containerd. Этот демон управляется утилитой командной строки docker-cli
Чтобы создать контейнер в Docker, нужен соответствующий образ. Образ контейнера содержит нужные файлы для процесса, включая зависимости нужных версий
Образы можно создавать, а можно использовать готовые образы из репозитория, которые включают нужное приложение. Чтобы запустить образ, используется команда:
docker run hello-world
Здесь hello-world - минимальный образ, который был загружен из репозитория Docker Hub (https://hub.docker.com/_/hello-world). При запуске запускается контейнер, который выводит обучающую информацию о Docker
Чтобы создать образ, используется Dockerfile - специальный файл, который описывает создание образа. Например, для hello-world файл Dockerfile выглядит так:
FROM scratch
COPY hello /
CMD ["/hello"]
Здесь инструкциями указано, что:
scratch (который представляет из себя пустой образ)hello в корень контейнера/helloВ Dockerfile также могут использоваться другие команды (*тык*)
Далее используется команда:
docker build -t hello-world:v1 .
Здесь hello-world:v1 - это имя и версия образа, а . - корневой путь контейнера, относительно которого выполняются команды в Dockerfile. По умолчанию docker build будет искать файл ./Dockerfile, но можно указать флагом -f ./otherfolder/Dockerfile другое местоположение
Чтобы узнать список загруженных и созданных образов, используется команда docker images:
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
hello-world:latest 5e2309035332 25.9kB 9.49kB U
Здесь указаны идентификатор образа и объем на диске
Команда docker image inspect <image-name>:<version> позволяет изучить образ:

Сам образ представляет из себя слои. Каждая инструкция в Dockerfile представляет слой, который описывает изменения файловой системы в образе. Далее после применения инструкции из Dockerfile запоминается хеш слоя
Если какая-либо команда в Dockerfile изменилась, то Docker берет предыдущий слой, на основе которого собирает новый образ. При запуске контейнера весь образ остается в режиме “только для чтения” - последующие изменения файлов в ходе работы контейнера сохраняются в слое контейнера, а при чтении используется механизм объединенной файловой системы
Далее образ можно запустить:
docker run -d -p 8000:80 --name <container-name> <image-name>
Здесь применены флаги:
-d (от detached) означает, что контейнер запускается в отсоединенном режиме-p означает, что порт 8000 локальной машины соотносится с портом 80 контейнера для доступа контейнера к сети--name устанавливает имя контейнера