Задача: Docker-хост на Mac с Apple Silicon, который ничего не стоит в простое. Это значит
ноль дискового I/O, CPU как можно ближе к нулю и RAM, которая возвращается macOS, когда не нужна.
При этом нужен полноценный Docker Engine API. Проект, ради которого всё делалось, завязан на
testcontainers, несколько файлов docker compose и werf, поэтому рантайм без
docker.sock не подходит.
container machine как Docker-хост| Машина | Apple M5 (4 производительных + 6 энергоэффективных ядер), 16 ГБ RAM |
| Хост | macOS 26.6.2 (25G83) |
| VMM | Lima 2.2.0, vmType: vz (Apple Virtualization.framework), 4 vCPU, 6 ГиБ, virtiofs, Rosetta включена |
| Гости | Alpine 3.23.4 cloud-образ (ядро 6.18.22 / 6.18.53) и ISO alpine-lima std v0.2.50 (Alpine 3.24.1, ядро 6.18.38) |
| Docker | 29.5.2 из apk (cloud) / 29.8.1 статическая сборка docker.com (ISO); CLI на хосте 29.5.2 |
| Нагрузка | postgres:17-alpine + pgbench, foundationdb/foundationdb:7.3.77 (движок ssd) + Python-биндинги 7.3.79 |
| Дата | 2026-09-24 |
Каждая цифра ниже получена за один прогон, без повторов и доверительных интервалов. Разницу в несколько процентов считайте шумом; см. Ограничения.
Этот раздел — кабинетное исследование (release notes, документация, issue-трекеры), а не бенчмарк. Руками проверены только Lima и Apple
container.
| Lima 2.2 (vz) | OrbStack 2.2.3 | Colima 0.10.3 | Podman 6.1 (libkrun) | Docker Desktop 4.92 | Apple container 1.4.1 |
|
|---|---|---|---|---|---|---|
Настоящий Docker Engine + docker.sock |
✅ | ✅ | ✅ (это и есть Lima) | ⚠️ эмуляция API | ✅ | ❌ |
Опубликованные порты на localhost |
✅ guestagent | ✅ | ✅ | ✅ | ✅ | ❌ у machine |
| Возвращает RAM в macOS | ❌ (#4220, #2789) | ✅ «динамическая память» (со слов производителя) | ❌ | — | частично | — |
| CPU в простое | ~1% ядра, замеры ниже | ~0,1% (со слов производителя) | как у Lima | — | выше | — |
| Лицензия | Apache-2.0 | закрытый код, платно для коммерческой работы | MIT | Apache-2.0 | платно для крупных компаний | Apache-2.0 |
werf, которому нужен Docker-демон. К тому же у его провайдера по
умолчанию на macOS, libkrun, есть открытые проблемы с bind-маунтами на запись.По Lima: в 2.2.0 исправлена утечка сокета на каждое проброшенное соединение (#5210, фикс
#5216). На долгоживущем инстансе она в итоге упиралась в RLIMIT_NOFILE, и проброс портов
молча умирал. Для testcontainers это важно: он открывает много соединений. Ещё в Lima 2.2
убрали template:_images/alpine, теперь шаблоны должны называть версию (_images/alpine-3.23).
container machine как Docker-хостПроверено на container 1.3.1, потом сверено с release notes 1.4.1. Конфигурация: свой
Alpine-образ, в котором dockerd слушает tcp://0.0.0.0:2375, запущенный через container
machine.
Что не работает, причём так задумано, а не из-за ошибок настройки:
container machine нет -p, поэтому на хосте понадобился
отдельный TCP-форвардер в userspace.--publish-socket есть у container run, но не у machine,
поэтому Docker API пришлось гонять по обычному TCP.ip addr add 192.168.64.200/24 dev eth0)./proc, /sys и cgroup2 до передачи PID 1, после
чего sysinit OpenRC падает на повторном монтировании и перезагружает VM. Приходится
использовать busybox init. container machine run --root не поддерживается.home-mount=rw монтирует в машину весь $HOME. Вместе с
dockerd без авторизации на мосту vmnet получается, что всё, что видит 192.168.64.0/24,
получает root в VM и доступ на запись в домашний каталог.1.3.1 → 1.4.1. Версию 1.4.0 отозвали. В 1.4.1 два исправления безопасности в
Containerization (обход каталога через симлинки при загрузке OCI-образа; переполнение длины
sun_path), команда container clean, расширенный container system status, слэши в JSON
больше не экранируются, Containerization обновлён до 0.45.0. Для сети machine ничего не
изменилось. Среди открытых запросов — пользовательские маунты (apple/container#1805,
#2278) и machine stats (#1919).
Идея: собрать гостя по образцу vminit от Apple или VM Docker Desktop, где нет ничего, кроме ядра,
init и dockerd.
lima-init, создание пользователя,
SSH-ключи и guest agent. В LinuxKit ничего из этого нет.linuxkit run virtualization — тестовый раннер. Консоль у него на stdio, сеть — NAT со
случайным IP, и он не пробрасывает ни vsock, ни опубликованные порты. Пример
docker-for-mac.yml из апстрима устарел (HyperKit, Docker 20.10) и запускает dockerd в
dind-контейнере поверх системного containerd, то есть процессов становится больше, а не меньше.virtio-vsock,socketURL= выставляет docker.sock на хост как unix-сокет. Но автоматический
проброс на localhost динамически публикуемых портов, который делает guest agent Lima и на
который опирается testcontainers, пришлось бы писать с нуля. Примерно так устроен
podman machine с Fedora CoreOS.dockerd (86 МБ RSS), containerd (68 МБ) и lima-guestagent (45 МБ); RSS учитывает общие
страницы, поэтому в сумме выходит больше итога. Всё, что обычно вырезают гайды по «минимальной
VM» (syslogd, acpid, getty, cloud-init-hotplugd, chronyd), вместе даёт около 4,7 МБ. Эти
цифры — из предыдущего раунда замеров (июль 2026, Lima 2.1).Ближе всего к этому из того, что Lima поддерживает штатно, — ISO alpine-lima. Сравнение ниже.
templates/minimal-docker-iso.yaml):
alpine-lima std, на нём построен Rancher Desktop. Корневая ФС — tmpfs, собирается заново при
каждой загрузке, cloud-init нет, его работу делает shell-скрипт lima-init. Сохраняются только
/etc /home /root /tmp /usr/local /var/lib (bind-маунты с /mnt/data), поэтому Docker
ставится статической сборкой docker.com в /usr/local.templates/minimal-docker.yaml): Alpine
nocloud qcow2 с cloud-init. Этот образ использует штатный шаблон Lima alpine.| ISO | Cloud, как есть | Cloud с правками (итоговый шаблон) | |
|---|---|---|---|
| Пустой простой, CPU VZ-процесса на хосте | 0,96% ядра | 1,31% | 1,00–1,12% |
| Запись на диск гостя в простое, 180 с (одна сессия) | 0 Б | 86 КБ за окно 120 с¹ | 0 Б |
RAM гостя после загрузки (used) |
133 МБ + 76 МБ корня в tmpfs | 148 МБ | — |
RSS dockerd / containerd |
79 / 41 МБ | 85 / 47 МБ | — |
limactl restart → READY |
~12 с | ~26 с | ~14 с |
| Можно менять командную строку ядра | ❌ зашита в ISO | ✅ grub | ✅ grub |
| Простой с запущенными FDB и Postgres | 3,88% | 4,13% | 3,77% |
| pgbench, 8 клиентов, tps | 15 495 | 15 084 | 14 497 |
| Запись в FDB, ключей/с | 245 тыс. | 246 тыс. | 253 тыс. |
¹ Замер через две отдельные сессии limactl shell, то есть вместе со входами; с цифрами по
одной сессии не сравнивается.
Вывод. После правок варианты одинаково тихие и одинаково быстрые. Решает командная строка
ядра: только в cloud-образе можно задать init_on_free=1, а это примерно вдвое уменьшает
RAM, которую VM реально удерживает на хосте после нагрузки (§6). ISO-вариант оставлен в
репозитории для тех, кому немутабельность важнее.
Единственный, кто пишет на диск в простаивающем госте, — lima-guestagent. Каждые 10 с он пишет
в лог SyncTime: system time synchronized with host (drift was 377.9ms). Дрейф никогда не
сходится, и цикл нельзя отключить конфигом (в Lima 2.2 это по-прежнему так, #5365 только
взял строку в кавычки). Около 600 Б/мин лога превращаются в ~25 КБ/мин блочной записи через
журнал ext4. Та же строка копится и на хосте в ~/.lima/<name>/ha.stderr.log (~900 КБ/сутки,
без ротации, обнуляется рестартом).
Решение — /var/log в tmpfs плюс commit=60, причём в provision: mode: boot, чтобы это
произошло до того, как guest agent откроет свой лог. mode: system уже поздно: демон
продолжит писать в старый inode. Результат — 0 байт за 180–240 с простоя. В ISO корень и так
в tmpfs, там это не нужно.
Профиль пробуждений гостя за 30 с в пустом простое, снят bench/wakeups.sh:
| ISO | Cloud, как есть | |
|---|---|---|
Прерывания arch_timer |
~102/с | ~157/с |
Переключения контекста lima-guestagent |
~22/с | ~24/с |
containerd |
~17/с | ~23/с |
rcu_preempt |
~1,3/с | ~9,5/с |
| прочее | — | log_proxy ~4/с, syslogd ~2/с, init ~2/с |
Правки в cloud-образе применялись по одной; после каждой — CPU VZ-процесса на хосте:
| Шаг | CPU на хосте |
|---|---|
| как есть | 1,31% |
| ядро 6.18.22 → 6.18.53 | 1,28%, то есть дело не в ядре |
cloud-init-hotplugd выключен, syslogd в кольцевой буфер |
1,27% |
getty на ttyAMA0 выключен |
1,00% |
inittab образа запускает getty на ttyAMA0, а такого устройства в VZ нет, поэтому busybox
init перезапускает его бесконечно. Это самое большое падение из замеренных.
Ещё:
rc-update del syslog ничего не даёт: use logger у
sshd возвращает его, когда Lima перезапускает sshd в конце загрузки. Решение —
SYSLOGD_OPTS="-t -C64": кольцевой буфер фиксированного размера 64 КБ, без файла, читается
через logread.GRUB_TIMEOUT=10 в cloud-образе добавляет 10 с к каждой загрузке.acpid, без него limactl stop превращается в hard kill с
последующим fsck.Остаток, около 1% одного ядра, — это в основном таймеры Go-рантайма в lima-guestagent и
containerd. Настройками Lima его не убрать. --tick (по умолчанию 3 с) — это только интервал
опроса портов.
Колонка IDLEW у top накопительная с момента старта процесса, сравнивать её между прогонами
нельзя. Смотрите на прирост CPU-времени (ps -o time).
Что видно. После одного прогона pgbench phys_footprint VZ-процесса стоит на полных
6 ГиБ. echo 3 > /proc/sys/vm/drop_caches в госте освобождает 5,6 ГБ внутри гостя, но
footprint на хосте остаётся 6158 МБ. У VZ нет free page reporting, а Lima не управляет balloon.
Как мерить правильно. phys_footprint (и колонка Memory в Activity Monitor) считает
сжатые страницы в несжатом размере, поэтому возврат памяти по ней не увидеть. Например,
vmmap показывал 4,9 ГБ из 6 ГБ VM в swapped_out (swap здесь нет, это значит «сжато»), а
footprint по-прежнему был 6 ГБ. Честная цифра — сколько памяти хост получает обратно, когда VM
останавливается: прирост free по vm_stat.
Эксперимент (bench/memtest.sh): нагрузка pgbench + FDB →
drop_caches в госте → 40 с давления на память хоста (memory_pressure -l warn) →
limactl stop.
Без init_on_free |
С init_on_free=1 |
|
|---|---|---|
| RAM, освобождённая на хосте остановкой VM (реальная цена VM) | 4193 МБ | 2054 МБ |
| Страницы VM в компрессоре → сколько места занимали | 5043 МБ → 3025 МБ (1,7×) | 4327 МБ → 238 МБ (18×) |
| pgbench tps / FDB ключей/с в том же прогоне | 15 084 / 246 тыс. | 15 713 / 242 тыс. |
Почему это работает: с init_on_free=1 ядро гостя обнуляет каждую освобождаемую страницу. Компрессор
XNU хранит страницы из одного повторяющегося значения почти бесплатно, поэтому, когда памяти
начинает не хватать, свободные страницы VM почти ничего не стоят. Но page cache — это занятая
память с настоящими данными. Поэтому итоговый шаблон запускает маленький сервис cache-trim: он
сбрасывает чистый page cache раз в 10 минут и только когда гость простаивает (load1 < 0,2).
Побочные эффекты:
memory:, потому что обнулённые страницы затронуты. Так и
должно быть: RAM забирается только под давлением.Без давления у macOS нет причин забирать память, и от этого ничего не теряется.
Итоговый шаблон, по results/05-final-template-idle-and-load.txt:
| pgbench -s 50, 8 клиентов × 60 с | 14 497 tps, средняя задержка 0,55 мс |
| pgbench, 1 клиент × 20 с | 3331 tps, средняя задержка 0,30 мс |
| FDB, 400 тыс. ключей × 100 Б, 8 потоков, 500 ключей на транзакцию | 253 тыс. ключей/с |
| FDB, задержка коммита одного ключа | p50 1,07 мс, p99 1,61 мс |
| FDB, чтение всего диапазона | 535 тыс. ключей/с |
| Простой с обоими контейнерами | 3,8% ядра хоста; ~33 КБ/с записи на диск гостя |
fdbserver постоянно крутятся свои таймеры, ~1,4%
CPU внутри гостя, и на него приходится большая часть этих 3,8%. Postgres в простое ест 0%.
Останавливайте docker stop то, чем не пользуетесь, а VM — limactl stop, когда не работаете.limactl и процесса VPN во время скачивания образов. В остальное время оба стояли около
нуля. Если registry доступны напрямую, правило direct для них это убирает.fstrim -a однажды сразу вернул 2 ГБ. Шаблон запускает его при каждой загрузке./var/lib/docker занимал 11,2 из 11,8 ГБ, так
что лимиты daemon.json на размер логов и GC для BuildKit важнее любого урезания ОС. (Эти две
цифры — из июльского раунда 2026.)base: работает только в шаблонах. В собственном lima.yaml
инстанса нужен явный images:, и limactl tmpl validate этого не ловит.init_on_free» сняты на одном
инстансе в разное время, и между ними обновлялось ядро (6.18.22 → 6.18.53).limactl start --name=dev ./templates/minimal-docker.yaml && limactl restart dev
docker context create lima-dev --docker "host=unix://$HOME/.lima/dev/sock/docker.sock"
./bench/idle.sh dev 120 60 # пустой простой
./bench/disk-writes.sh dev 180 # запись на диск в простое, одна сессия
./bench/wakeups.sh dev 30 # кто будит гостя
./bench/load.sh dev # FDB + Postgres: простой с контейнерами, нагрузка, простой после
./bench/memtest.sh dev # реальная цена RAM после нагрузки (останавливает VM, давит на память хоста)
Сырые выводы всех упомянутых прогонов лежат в results/.