Tux с новой игрушкой
Tux с новой игрушкой

Технологии контейнеризации, возможно, как и у большинства читателей данной статьи, плотно засели в моей голове. И казалось бы, просто пиши Dockerfile и не выпендривайся. Но всегда же хочется узнавать что-то новое и углубляться в уже освоенные темы. По этой причине я решил разобратьсяв реализации контейнеров в ОС на базе ядра linux и в последствие создать свой «контейнер» через cmd.

На ком держатся контейнеры в Linux?

На страже
На страже

Для начала необходимо понять на чем именно основана технология контейнеризации. В ядре Linux существуют два механизма: namespace (пространство имен), cgroups (control groups). Они обеспечивают изоляцию и масштабируемость, за которые мы все так любим контейнеры. Давайте разберем по порядку оба механизма.

Namespace

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

Рассмотрим контейнер поднятый из образа alpine. Запустим его и интерактивную оболочку в нем:

docker run -it alpine /bin/sh

Теперь создадим новый процесс в контейнере, и проверим вывод команды ps:

sleep 1000 &
ps -a

Получаем:

PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   29 root      0:00 sleep 1000
   30 root      0:00 ps -a

Обратите внимание, что PID процесса равен 29. Теперь попробуем найти этот же процесс, но на хостовой машине. Для этого определим ID контейнера и воспользуемся командой для отображения процессов, запущенных внутри docker:

docker top <ID контейнера>

В результате получаем:

UID     PID       PPID      C    STIME    TTY      TIME        CMD
root    172147    172124    0    Feb05    pts/0    00:00:00    /bin/sh
root    173602    172147    0    Feb05    pts/0    00:00:00    sleep 1000

Обратим внимание на 2 столбца: PID и PPID(parent PID). Они указывают на PID самого процесса и родительского, но уже в хостовой системе. Давайте проверим это:

ps aux | grep -E '173602|172147'

Получаем:

root      172147  0.0  0.0   1736   908 pts/0    Ss+  Feb05   0:00 /bin/sh
root      173602  0.0  0.0   1624   980 pts/0    S    Feb05   0:00 sleep 1000

Что и требовалось доказать! Если подытожить, то можно сделать вывод, что контейнер ничего не знает про хостовую машину. Он считает, что является самостоятельной системой. Однако, в действительности все процессы запущены на хосте, просто они находятся в пространстве имен данного контейнера. Это и создает иллюзию отдельной независимой системы.

Надеюсь, данный пример чуть прояснил ситуацию с namespace. В нем мы разобрали один из 8 видов пространств имен. Теперь хотелось бы кратко пройтись по каждому:

  1. Mount - изоляция точек монтирования файловой системы. Позволяет установить свою иерархию фс;

  2. UTS - изоляция имени хоста. Позволяет для каждого контейнера указать свое хостовое имя;

  3. PID - изоляция идентификаторов процессов. Позволяет создавать отдельное дерево процессов;

  4. Network - изоляция сетевых интерфейсов, таблиц маршрутизации;

  5. IPC - изоляция IPC(межпроцессные взаимодействия);

  6. User - изоляция пользователей системы. Позволяет создавать отдельных пользователей для каждого контейнера, в том числе и root;

  7. Cgroup - изоляция доступа к cgroup. Позволяет ограничивать ресурсы контейнера и предотвращает вмешательства других контейнеров;

  8. Time - изоляция системного времени.

Для создания нового namespace в Linux существует команда unshare. С ней мы чуть позже познакомимся ближе.

Cgroups

Control groups - механизм ядра Linux, позволяющий управлять ресурсами процессов. С его помощью можно ограничить и изолировать использование CPU, памяти, сети, диска.

Существует две версии cgoups: v1 и v2. В большинстве современных систем вы встретите вторую версию, которая используется в работе systemd. Основное отличие версий в построении дерева ограничений. В первой версии создавались узлы для каждого вида ограничений, а в них уже добавлялись группы. Во второй версии для каждой группы свой узел, внутри которого все необходимые ограничения. Чтобы лучше понять, давайте взглянем на визуализацию деревьев v1 и v2:

#v1
/sys/fs/cgroup/
├── cpu
│   ├── group1/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── cpu.shares
│   │   └── ...
│   ├── group2/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── cpu.shares
│   │   └── ...
│   └── ...
├── memory
│   ├── group1/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── memory.limit_in_bytes
│   │   └── ...
│   ├── group2/
│   │   ├── tasks
│   │   ├── cgroup.procs
│   │   ├── memory.limit_in_bytes
│   │   └── ...
│   └── ...
└── ...

#v2
/sys/fs/cgroup/
├── group1/
│   ├── cgroup.procs
│   ├── cpu.max
│   ├── cpu.weight
│   ├── memory.current
│   ├── memory.max
│   └── ...
├── group2/
│   ├── cgroup.procs
│   ├── cpu.max
│   ├── cpu.weight
│   ├── memory.current
│   ├── memory.max
│   └── ...
└── ...

Теперь давайте взглянем на работу cgroup на примере контейнера docker. Для начала запустим контейнер, ограничив его ресурсы(2 ядра и 512МБ):

docker run -d --cpus="2" --memory="512m" nginx 

Далее найдем группу для этого контейнера, воспользовавшись командой find :

find /sys/fs/cgroup -name '*<ID контейнера>*'

Далее проверим содержание файлов cpu.max и memory.max в найденной директории:

# cpu.max
200000 100000

# memory.max
536870912

Что и требовалось доказать!

Создание контейнера без docker

Tux волшебник?!
Tux волшебник?!

Мы разобрались с основной необходимой нам теорией. Теперь перейдем к практике и прибегнем к волшебству командной строки.

Для начала создадим структуру файловой системы контейнера, установим busybox в директорию /bin :

# Создаем корневую дирекотрию контейнера и переходим в нее
mkdir ~/container && cd ~/container
# Создаем основные системные директории и переходим в /bin
mkdir -p ./{proc,sys,dev,tmp,bin,root,etc} && cd bin
# Устанавливаем busybox
wget https://www.busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
# Выдаем право на исполнение
chmod +x busybox
# Создаем симлинки для всех команд, которые есть в busybox 
./busybox --list | xargs -I {} ln -s busybox {}
# Возвращаемся в корневую директорию контейнера
cd ~/container
# Добавляем переменную PATH в файл /etc/profile
echo 'export PATH=/bin' > ~/container/etc/profile

Также добавим в файлы /etc/passwd и /etc/group, чтобы внутри изолированной системы мы были рутом:

echo "root:x:0:0:root:/root:/bin/sh" > ~/container/etc/passwd
echo "root:x:0:" > ~/container/etc/group

Далее смонтируем системные директории:

# Монтируем устройства, используя уже существующие
sudo mount --bind /dev ~/container/dev
# Монтируем процессы
sudo mount -t proc none ~/container/proc
# Монтируем файловую систему sysfs
sudo mount -t sysfs none ~/container/sys
# Монтируем файловую систему tmpfs
sudo mount -t tmpfs none ~/container/tmp

Примечание: для того чтобы потом размонтировать можно воспользоваться командой:

sudo umount ~/container/{proc,sys,dev,tmp}  

Мы подготовили файловую систему для нашего контейнера. Теперь перейдем к созданию изоляции. Для этого мы воспользуемся командой:

unshare -f -p -m -n -i -u -U --map-root-user --mount-proc=./proc \
    /bin/chroot ~/container /bin/sh -c "source /etc/profile && exec /bin/sh"

Давайте разберем ее подробнее:

  • -f - fork. Создаем новый процесс для изоляции от родительского;

  • -p - PID namespace;

  • -m - mount namespace;

  • -n - Network namespace;

  • -i - IPC namespace;

  • -u - UTS namespace;

  • -U - User namespace;

  • --map-root-user - маппинг uid и gid активного пользователя на root внутри контейнера;

  • -mount-proc - монтируем proc внутри контейнера;

  • /bin/chroot ~/container - меняем корневую директорию;

  • /bin/sh -c "source /etc/profile && exec /bin/sh" - запускаем shell и исполняем команду, которая применит файл /etc/profile и запустит интерактивный shell.

Отлично! Мы получили свой контейнер. Теперь осталось ограничить ресурсы. Для этого откроем новую сессию на хосте и выполним ряд действий:

# Создаем новую группу. В моей системе используется cgroups v2, поэтому
# директория автоматически будет настроена для работы с ресурсами
sudo mkdir /sys/fs/cgroup/my_container
# Записываем ограничение на 2 ядра процессора
echo "200000 100000" | sudo tee /sys/fs/cgroup/my_container/cpu.max
# Выделяем максимум 512MB памяти
echo 536870912 | sudo tee /sys/fs/cgroup/my_container/memory.max

Далее необходимо определить PID контейнера, для этого воспользуемся командой:

ps aux | grep -E '/bin/sh$' 

Берем PID из второго столбца и добавляем в файл cgroup.procs :

echo <PID> | sudo tee /sys/fs/cgroup/my_container/cgroup.procs

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

# Создаем пару виртуальных интерфейсов
sudo ip link add veth-host type veth peer name veth-container
# Поднимаем интерфейс на хосте
sudo ip link set veth-host up
# Назанчаем любой свободный адрес в вашей сети на интерфейс хоста
# Я использую 192.168.1.123/24
sudo ip addr add 192.168.1.123/24 dev veth-host
# Перемещаем veth-container в пространство имен контейнера
# Здесь нужно указать PID контейнера, который использовали до этого
sudo ip link set veth-container netns <PID>
# Поднимаем интерфейс внутри контейнер
sudo nsenter --net=/proc/<PID>/ns/net ip link set veth-container up
# Назанчаем любой свободный адрес в вашей сети на интерфейс контейнера
# Я использую 192.168.1.124/24
sudo nsenter --net=/proc/<PID>/ns/net ip addr add 192.168.1.124/24 dev veth-container
# Настраиваем шлюз по умолчанию для маршрутизации трафика
sudo nsenter --net=/proc/<PID>/ns/net ip route add default via 192.168.1.123

Мы подняли все нужные интерфейсы. Теперь необходимо настроить маршрутизацию:

# Разрешаем пересылку пакетов
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# Добавляем правило NAT для маскардинга для исходящих пакетов из сети 
# 192.168.1.0/24 через интерфейс который смотрит во внешнюю сеть. У меня это enp3s0.
# Маскардинг маскирует пакеты исходящие их контейнера так, чтобы они выглядели,
# как пакеты отправленные с хоста
sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o enp3s0 -j MASQUERADE
# Добавляем парвило на разрешение форвардинга пакетов
sudo iptables -A FORWARD -s 192.168.1.0/24 -o enp3s0 -j ACCEPT
# Добавляем парвило, разрешающее входящие пакеты.
sudo iptables -A FORWARD -d 192.168.1.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT

Отлично! Мы создали свой первый контейнер. Понятно, что в нем еще много чего можно настроить, тот же DNS, который сейчас не работает. Но это уже каждый сам решит, как с этим играть.

Спасибо за прочтение!

Комментарии (39)


  1. Ascard
    11.02.2025 13:42

    Было бы неплохо ещё в конец статьи добавить как это дело всё прибить если что-то внутри пойдёт не так. И как удалить, не снеся случайно половину хостовой системы. Докер как-то это сам делает, а тут есть риск появления упсь!-моментов у начинающих контейнероводов.
    А за статью спасибо, небезынтересно было узнать как оно работает.


    1. lantonov
      11.02.2025 13:42

      Согласен как-то с докером спокойней это делать тан как в докер есть готовые файловые системы и методы удаления и создания контейнера есть ещё более взрослый вариант то lxd



  1. vikarti
    11.02.2025 13:42

    Интересно а кто нибудь еще помнит что такое Virtuozzo и позднее - OpenVZ и какое это отношение имеет с тем же cgroups/namespaces ?


    1. datacompboy
      11.02.2025 13:42

      Такое же как и linux-vservers. Прородители :)


    1. Tsimur_S
      11.02.2025 13:42

      Туда же freebsd jail и solaris zones. ЕМНИП это еще прошлый век был.


    1. Self_Perfection
      11.02.2025 13:42

      Ну и про LXC и lxd/incus не забываем


  1. mnnoee
    11.02.2025 13:42

    Жаль что мало кто уделяет всему этому внимание, но кхе кхе. Есть отличный проект для этих нужд. Rurima. Скм и пользуюсь, может что-то можно будет подметить для себя в качестве запуска докера без докера.


  1. Stanislavvv
    11.02.2025 13:42

    Что-то никто не вспоминает https://github.com/p8952/bocker — Docker implemented in around 100 lines of bash.


    1. datacompboy
      11.02.2025 13:42

      1. Stanislavvv
        11.02.2025 13:42

        Кластер, однако. На момент отправки ещё не видел.


  1. domix32
    11.02.2025 13:42

    Меня интересует несколько иной вопрос - вот у нас есть образа с alpine, void, arch, ubuntu - как они выглядят с точки зрения докера и исполняются в контексте конкретных дистров, а не как кусок хостовой системы.


    1. mayorovp
      11.02.2025 13:42

      А что именно непонятно? Установленный дистрибутив - это набор файлов, разложенных определённым образом в задуманные места. Внутри контейнера этот набор файлов выглядит прямо как набор файлов...


      1. domix32
        11.02.2025 13:42

        Ну, когда вы запускаете дистрибутив у него определённо идёт стадия загрузки, проверяются всякая переферия, поднимаются ramfs чтобы загрузить всякое, запускаются всякие стартовые программы. С точки зрения гостевой системы как это выглядит? Словно у нас уже прошли эти этапы и мы уже всё запустили и проинициализировали или он тоже честно стартует загрузку с шага загрузчика?


        1. gudvinr
          11.02.2025 13:42

          Этим занимается загрузчик и ядро. С точки зрения "хостовой" системы это выглядит точно так же, то есть никак. Потому что, грубо говоря, "хостовая" система - это chroot из initrd, а контейнер - это chroot из "хостовой" системы, опять же, очень условно.

          Вы точно так же можете загрузиться с USB, сделать chroot в свой корень и пользоваться этой системой поверх того ядра, которое было на USB


        1. redfox0
          11.02.2025 13:42

          В "гостевой" системе это выглядит как запуск бинарника. Всё. Этим бинарником может быть ваш исполняемый файл, bash, выполняющий скрипт, или даже init systemd (для podman). Последний несколько забавен, так как понимает, что запущен в контейнере, присасывается в хостовому systemd и льёт туда логи. Ну и безопасно позволяет остановить демоны внутри контейнера при остановке всего контейнера.


        1. mayorovp
          11.02.2025 13:42

          Стадии загрузки в контейнере нет, контейнер начинается сразу с запуска основного процесса (в дистрибутивах это обычно шелл).

          Да блин, ну вы же комментируете статью, где рассказано как контейнеры запускаются! В какой момент автор поднимал ramfs и инициализировал периферию? Да ни в какой, он сразу /bin/sh в контейнере запустил! При этом он скачал busybox, а мог скачать образ дистрибутива - остальные шаги никак бы не поменялись.


          1. domix32
            11.02.2025 13:42

            А в чем прикол иметь образа с чем-то кроме того же busybox? Специфичный лэйаут файловой системы, конфигов и состава бинарей и shared либ?


            1. baldr
              11.02.2025 13:42

              Я бегло погуглил и некоторые причины есть.

              Busybox is amazing, if you want small. If you want featurefull, GNU is way better.

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

              Лицензия. Это GPL и они уже несколько раз судились за нарушения.

              Ну и то что вы перечислили - тоже.


            1. mayorovp
              11.02.2025 13:42

              Ну, кроме busybox всё-таки нужны ещё и либы, иначе ничего полезного вы не запустите. И пакетный менеджер, чтобы поставить те либы и инструменты, которых не оказалось в образе.


              1. baldr
                11.02.2025 13:42

                Всё это можно собрать из исходников. Тот же busyBox модульный и можно включить дополнительные компоненты, или убрать ненужные, втч пакетный менеджер - именно поэтому он так популярен для embedded платформ.


                1. mayorovp
                  11.02.2025 13:42

                  Вот делать нечего кроме как собирать чужой софт из исходников. Докер выбирают ради ускорения и упрощения CI/CD, а вы предлагаете дальше замедлять.

                  Плюс для сборки софта нужны инструменты, голым busybox его не собрать.

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


                  1. baldr
                    11.02.2025 13:42

                    Я не совсем понимаю что вы хотите всем этим сказать. Инструменты сборки и пакетный менеджер внутри готового образа - это потенциальная угроза безопасности. Если что-то нужно собрать или поставить - используются multi-stage build.

                    На многих прошивках роутеров (например, Teltonika) идёт просто голый BusyBox, внутри которого один собранный бинарник, но там есть и веб-админка, и консоль.


                    1. mayorovp
                      11.02.2025 13:42

                      Причём тут вообще прошивки роутеров? Вы используете докер на роутерах или где? И даже если используете - неужели вы думаете что только для роутеров он и годится?

                      multi-stage build - вещь хорошая, и используется постоянно, однако проблему не решает. Во-первых, пакетный менеджер вам всё равно понадобится, потому что зависимостей у каких-нибудь .NET или Java - до чёрта, и вручную все их копировать вам не захочется. А во-вторых, не каждый пакетный менеджер имеет ставить пакеты по отдельному префиксу, в котором не стоит он сам - а значит, и забрать их все автоматически в следующую стадию multi-stage build вы не сможете. Наконец, в-третьих, если вы так и сделаете, то быстро обнаружите насколько распухли все ваши образы.

                      Инструменты сборки и пакетный менеджер внутри готового образа - это потенциальная угроза безопасности

                      А пакетный менеджер на сервере - не угроза? Что именно может сделать злоумышленник внутри контейнера, чего не может сделать снаружи?


                      1. baldr
                        11.02.2025 13:42

                        Причём тут вообще прошивки роутеров? Вы используете докер на роутерах или где? И даже если используете - неужели вы думаете что только для роутеров он и годится?

                        Смотрите - мой аргумент в этой ветке был, в первую очередь, про embedded-платформы и, да - роутеры. Я работал с Teltonika - там именно BusyBox в контейнере, но в комплекте с веб-админкой идёт и ModBus, и MQTT, и файрволл, и ещё дочерта софта. Почти всё собирается в единый бинарник.

                        Пакетный менеджер может отсутствовать. И, в принципе, должен бы отсутствовать - зачем в готовом имидже пакетный менеджер или gcc или какой-нибудь build-essentials - не будет же приложение в рантайме его вызывать? А вот если в контейнер пролезет через какие-то уязвимости троян - ему это может пригодиться.

                        Конечно же, вы можете использовать пакетный менеджер чтобы установить либы и всё остальное для multi-stage build - тот же BusyBox вы можете собрать хоть под Debian, хоть под RedHat.

                        Я уверен что вы всё это понимаете, просто у нас с вами какой-то странный спор.


                      1. mayorovp
                        11.02.2025 13:42

                        Смотрите - мой аргумент в этой ветке был, в первую очередь, про embedded-платформы и, да - роутеры.

                        Вот я и не понимаю, причём тут вообще embedded-платформы?

                        Конечно же, вы можете использовать пакетный менеджер чтобы установить либы и всё остальное для multi-stage build - тот же BusyBox вы можете собрать хоть под Debian, хоть под RedHat.

                        BusyBox и собирать-то не нужно, у него собранный бинарник выложен. Вы лучше расскажите как на чистый образ поставить JRE со всеми зависимостями. И сколько времени вам потребуется на написание докерфайла.


                      1. baldr
                        11.02.2025 13:42

                        Вот я и не понимаю, причём тут вообще embedded-платформы?

                        А я не понимаю при чём здесь JRE. Мы с вами сидим в ветке про BusyBox. Он широко применяется в embedded платформах.

                        BusyBox и собирать-то не нужно, у него собранный бинарник выложен

                        Он выложен для какой-то дефолтной конфигурации. Обычно он собирается для конкретных целей:

                        It is also extremely modular so you can easily include or exclude commands (or features) at compile time. This makes it easy to customize your embedded systems.


              1. domix32
                11.02.2025 13:42

                Ну, кажется пошарить либы не настолько большая проблема. Сделать условный ldd, скопировать so в контейнерный /usr/lib и в ус не дуть. Это очень скудно поясняет почему кому-то нужен образ с Fedora или Ubuntu, а другому Alpine за глаза.


                1. datacompboy
                  11.02.2025 13:42

                  обычно помимо либ надо еще россыпь полезных файлов из etc & usr/share, типа нужной таймзоны, корневых сертификатов и так далее


                1. mayorovp
                  11.02.2025 13:42

                  Вам ldd покажет только те разделяемые библиотеки, которые требуются для запуска. Те, которые догружаются во время работы, так просто не увидеть.

                  И не забывайте что некоторые странные программы используют утилиты командной строки. Не просто так проекты MinGW и Cygwin содержат, помимо библиотек, полные наборы утилит GNU - вызовы system("...") могут оказаться в кодовой базе в самых неожиданных местах.


            1. YouROK
              11.02.2025 13:42

              Ни в чем, только удобство. Если нужно в систему поставить программу, то проще через менеджер контейнеров сделать.
              У меня есть контейнер в котором только одна программа

              FROM debian:bookworm-slim as builder
              RUN mkdir /src
              COPY ./ /src
              RUN /src/mk.sh

              FROM scratch
              COPY --from=builder /app/TorrServer /
              WORKDIR /
              ENTRYPOINT [ "/TorrServer" ]


  1. Kahelman
    11.02.2025 13:42

    Для любителей есть ещё User Mode Linux, когда весь Linux в отдельный процесс пакуется который можно как обычный файл запускать :)


    1. inkelyad
      11.02.2025 13:42

      Это уже не контейнер. Потому что у полученной системы ядро таки свое - тот самый запущенный бинарник.


      1. Kahelman
        11.02.2025 13:42

        И что? Контейнер это не про ядро а про изоляцию программ. Вы же не ядра запускаете а программы. То что они все на одном ядре, так вы обычно на линуксовой машине линуксовые машины запускаете.


        1. inkelyad
          11.02.2025 13:42

          И что? Контейнер это не про ядро а про изоляцию программ.

          Я так не считаю. Термины хорошо бы все-таки использовать в соответствии с тем, что они означают. А то так какой-нибудь Xen тоже контейнером называть что ли?


        1. mayorovp
          11.02.2025 13:42

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

          User Mode Linux всё-таки кооперативная виртуализация (она же паравиртуализация).


  1. redfox0
    11.02.2025 13:42

    По названию статьи подумал, что кто-то открыл для себя podman или низкоуровневые среды выполнения контейнеров (OCI), такие как runc и crun.


  1. slonopotamus
    11.02.2025 13:42

    sudo mount --bind /dev ~/container/dev

    Вот это немного перебор, имея права рута в контейнере можно пошалить с хостовыми устройствами.


  1. devoln
    11.02.2025 13:42

    Я тоже пишу свой аналог Docker, только без неймспейсов и cgroups, чтобы его можно было запускать на рутованных (chroot + overlayfs) и нерутованных (proot + аналог vfs) смартфонах. Возможно, добавлю опциональную поддержку cgroups, если удастся заставить их работать на моём смартфоне - они вроде есть, но в каком-то урезанном виде.

    Мой проект называется nocker. Наверное на этой неделе залью на GitHub. Написан на чистом POSIX shell с минимальными зависимостями (wget, jq). Реализовал pull, run, start, exec, ps, rm, inspect и ещё несколько команд. Для моих задач не хватает только build и compose.

    Уже получилось 1500 строк кода. Странно как у bocker с близким функционалом получилось уложиться всего в 120 строк. Надо поподробнее изучить его код и сравнить с моим.