С чем столкнулся#
У меня на основном Docker-хосте долгое время была одна общая bridge-сеть с условным названием proxy, в которой сидели все стэки и обратный прокси Traefik - тоже в виде обычного Docker-контейнера, подключённого к этой же сети.
Потом я перенёс Traefik из Docker в отдельный LXC-контейнер, где он работает уже не как контейнер, а как обычный бинарник (подробно расписано в серии статей про установку Traefik в LXC: часть 2 про финальный конфиг, часть 3 про CrowdSec рядом с ним и часть про firewall про вынос всего этого в отдельный VLAN). И тут схема с общей сетью перестала работать в принципе. Traefik больше не Docker-контейнер, а значит он физически не может подключиться к docker-сети через external: true, как раньше, - у него просто нет доступа внутрь докеровской сетевой изоляции.
Пришлось перекраивать всё заново. Хотя более правильно сказать оптимизировать. Общую сеть с прокси я убрал, а каждый docker-compose стэк получил свою собственную отдельную сеть.
Побочным эффектом это дало и классическую изоляцию: контейнеры одного стэка видят друг друга, а контейнеры разных стэков - вообще не видят друг друга напрямую, ведь общей сети с Traefik больше не существует как класса.
Пока я по очереди пересоздавал стэки под новую схему, в какой-то момент docker compose up перестал подниматься с ошибкой:
Error response from daemon: could not find an available, non-overlapping IPv4 address pool among the defaults to assign to the networkИногда было то же самое но несколько видоизмененном виде:
failed to create network <имя>_default: Error response from daemon: could not find an available, non-overlapping IPv4 address pool among the defaults to assign to the networkТо есть Docker физически не может создать ещё одну сеть - у него закончились свободные диапазоны адресов.
Ко всему прочему еще и другие docker-сети переставали подниматься с той же самой ошибкой - то есть проблема была не в одном конкретном стэке, а системная, во всём пуле адресов сразу.
Почему это происходит#
Когда сеть создаётся без явно указанной подсети - subnet, Docker берёт её из внутреннего пула - так называемого default-address-pools. По умолчанию это диапазон 172.17.0.0/12 (то есть весь блок 172.16.0.0–172.31.255.255), из которого каждой новой сети нарезается часть размером /20.
Кусок /20 - это 4096 адресов на одну сеть. Даже если в стэке два контейнера, Docker всё равно выделит ему такой блок. При таком размере куска из /12 помещается всего около 15–16 сетей - и это без учёта того, что часть диапазона может пересекаться с адресами, которые уже заняты в системе (например VPN, другие интерфейсы и т.д.).
Именно поэтому переход на схему «своя сеть каждому стэку» - это ровно тот сценарий, где лимит наступает очень и очень быстро. Один общий bridge на всё - одна сеть. С десяток стэков с отдельными сетями - десяток сетей, и лимит по умолчанию оказывается где-то совсем рядом.
Диагностика#
Первым делом стоит убедиться, что дело действительно в исчерпании пула, а не в разовом конфликте с конкретной подсетью. Смотрю, сколько сетей уже создано и какие диапазоны они занимают:
docker network ls
docker network inspect $(docker network ls -q) -f '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}}{{end}}'Если в выводе видно, что уже занята большая часть диапазона 172.16.0.0/12 кусками по /20, а новых сетей становится всё больше - это оно.
Также стоит проверить, не мешает ли что-то ещё в сети хоста (VPN-клиент, дополнительный интерфейс), поднимающее маршруты в том же диапазоне - тогда пересечение начнётся ещё раньше, чем реально закончится пул адресов. У меня причиной послужило именно количество сетей.
Решение#
Пул адресов и размер куска на одну сеть настраиваются в /etc/docker/daemon.json через ключ default-address-pools. Я задал следующее значение:
{
"default-address-pools": [
{ "base": "172.20.0.0/12", "size": 24 }
]
}Смысл в двух деталях:
- Кусок размером
/24вместо дефолтного/20- это 256 адресов на сеть вместо 4096. Для обычного docker-compose стэка этого более чем достаточно. - Пул
/12, нарезанный на/24, даёт порядка 4096 доступных сетей вместо изначальных полутора десятков.
После правки конфига нужен рестарт демона:
sudo systemctl restart dockerРестарт демона Docker не трогает уже запущенные контейнеры напрямую, но пересоздаёт сетевой стек - на практике я делал это в окно с минимальным трафиком. Говоря человеческим языком - ночью. После проверял, что все контейнеры и их сети поднялись штатно. Если у вас продакшн-нагрузка - планируйте рестарт как отдельный шаг, а не «на ходу».
После рестарта новые сети уже нарезаются из нового пула, а docker compose up для оставшихся стэков прошёл без ошибок.
На что обратить внимание, если планируете то же самое#
- Если вы переходите с общей сети на отдельную сеть под каждый стэк - сразу закладывайте
default-address-poolsс меньшим размером куска (/24или даже/28для совсем мелких сетей), а не ждите, пока упрётесь в лимит на середине миграции. baseможно выбрать любой приватный диапазон, не пересекающийся с вашей физической сетью и VLAN’ами - если у вас уже используется часть172.x.x.xпод что-то ещё (например, под VLAN’ы, как у меня), проверьте, что диапазон вdaemon.jsonс ними не пересекается.- Изменение
default-address-poolsдействует только на новые сети - уже существующие сети сохраняют свои старые (более широкие) подсети, пересоздавать их не обязательно. - Удалённые, но не подключённые ни к одному контейнеру сети (
docker network prune) освобождают место в пуле - если лимит подкрался предательски внезапно, для начала стоит проверить, не накопился ли мусор из старых сетей.
Итог#
Ограничение по числу Docker-сетей - это не какой-то отдельный «лимит», а прямое следствие того, как Docker по умолчанию нарезает адресный пул: большими кусками из небольшого диапазона. Как только сетей становится больше десятка, дефолтных настроек перестает хватать. Решение простое, делается один раз - сузить размер выделяемой каждой сети подсети через default-address-pools в daemon.json, и лимит отодвигается на тысячи сетей вперёд. Для домашней лабы это уже фактически означает отсутствие лимита.
Мой вариант, по сути, побочный эффект от более крупной миграции - переноса Traefik из Docker-контейнера в отдельный LXC как systemd-бинарник. Более подробно об этом:





