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

Адрес удалённого Docker контейнера нам известен - 172.23.1.2. Таким образом мы видим что он принадлежит подсети 172.23.0.0/16, используемой докером по-умолчанию. Осталось проверить локальные сети, созданные им. Для этого в консоли выполним команду:

$ docker network ls

NETWORK ID     NAME       DRIVER    SCOPE
b37a8f1d9e0e   bridge     bridge    local
3ddf7cc795c7   dev_dev    bridge    local
ce4290925aae   host       host      local
53771bac0ba2   work_dev   bridge    local

Чтобы удостовериться в конфликте, нужно пройтись по каждому имени сети командой docker network inspect <name>:

$ docker network inspect bridge

[
    {
        "Name": "bridge",
        "Id": "b37a8f1d9e0ec568217fd07d7e3264ecb1ce2227e223cd2afd9e75171ab5dcd8",
        "Created": "2023-07-12T08:34:22.064632755Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.66.0.0/24",
                    "Gateway": "10.66.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

Здесь нас интересует ключ IPAM.Config.Subnet. В данном случае видим что пересечений с адресом 172.23.1.2 нет, значит, проверяем дальше.

При запуске команды на MacOS или Ubuntu можно сократить вывод информации до минимума, применив grep:

$ docker network inspect bridge | grep Subnet

Следом проверяем вторую сеть из списка с именем dev_dev:

$ docker network inspect dev_dev

[
    {
        "Name": "dev_dev",
        "Id": "3ddf7cc795c728e6db05d60a21d411e56537244a18ce4367f1aad1a5adfd85a3",
        "Created": "2023-07-11T10:09:31.915263768Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.23.0.0/16",
                    "Gateway": "172.23.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "dev",
            "com.docker.compose.project": "dev",
            "com.docker.compose.version": "2.19.1"
        }
    }
]

Вот и наша злосчастная подсеть 172.23.0.0/16, перекрывающая внешние сервисы.

Мне удалось найти два основных решения данной проблемы:

  1. Пересоздавать сеть до тех пор, пока она не получит не конфликтный пул IP-адресов;

  2. Изменить диапазон используемых докером подсетей.

Пересоздание сети

Docker автоматически выбирает незанятый собой пул IP-адресов в диапазоне 172.0.0.0/8. Таким образом, можно удалить текущую сеть и создать её заново:

$ docker network rm dev_dev
dev_dev

$ docker network create dev_dev
6e94416358eee95cf5b6765804214c67851db737e15ce37c2c50c005365eb758

# Эта команда выведет список контейнеров, из которых нужно взять значение
# колонки NAMES для подключения
$ docker ps -a

CONTAINER ID   IMAGE                                 NAMES
297cd1e629a8   erikdubbelboer/phpredisadmin:latest   dev-redis-webui-1
69f0f0e3a767   redis:alpine                          dev-redis-1
1cc11f750fb6   dev-mysql                             dev-mysql-1

$ docker network connect dev_dev dev-mysql-1
$ docker network connect dev_dev dev-redis-1
$ docker network connect dev_dev dev-redis-webui-1

Если не создавать вручную сеть и не назначить её контейнерам, то получим сообщение:

Cannot start Docker Compose application. Reason: compose [start] exit status 1. Container dev-mysql-1 Starting Container dev-redis-1 Starting Error response from daemon: network 3ddf7cc795c728e6db05d60a21d411e56537244a18ce4367f1aad1a5adfd85a3 not found

При проверке выданного пула видим, что теперь адрес контейнера из локальной сети (172.23.1.2) не конфликтует с выданным:

$ docker network inspect dev_dev
[
    {
        "Name": "dev_dev",
        "Id": "6e94416358eee95cf5b6765804214c67851db737e15ce37c2c50c005365eb758",
        "Created": "2023-07-12T09:31:13.976800597Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "1cc11f750fb63b5a7d6f8892cf09607df5ddd87cadfc9a8c0c1af19d96f7f0af": {
                "Name": "dev-mysql-1",
                "EndpointID": "8a8060743e9b7bcf99b16c23a2c85ac0897d6153e8fe4ad9a43e8ec942682b9d",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            },
            "297cd1e629a888866009681e53357bcef7e1be03d433322b78753ca7856779f5": {
                "Name": "dev-redis-webui-1",
                "EndpointID": "18125f8d31f7f76e44dae64fe264fa369d82fd3a2e56ba8915bf53cbc642b56c",
                "MacAddress": "02:42:ac:11:00:04",
                "IPv4Address": "172.17.0.4/16",
                "IPv6Address": ""
            },
            "69f0f0e3a76779cbb9f44cafe6ca5370992aef71fce40edbc369b535d9c6a513": {
                "Name": "dev-redis-1",
                "EndpointID": "06e75846ee2edd7bd0663ccaeb772a4472930bb6f10c9ef14049307174aeada9",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Проблема этого метода в том, что при каждом возникшем конфликте требуется повторять эти действия. Особенно это касается случаев, когда контейнеры пересоздаются очень часто. Поэтому есть второй, более простой, вариант:

Ограничение пула адресов через конфигурирование Docker

Для реализации этой задумки нужно изменить файл daemon.json. В Unix системах он находится по пути /etc/docker/daemon.json, а под управлением Windows - %USERPROFILE%\.docker\daemon.json.

В случае использования Docker Desktop найти файл ещё проще: открываем настройки и переходим на вкладку Docker Engine. Всё. Содержимое можно изменять.

Просто впишите в этот JSON новый ключ конфигурации:

{
  "default-address-pools":[
    {"base":"10.66.0.0/16","size":24},
    {"base":"10.77.0.0/16","size":24}
  ]
}

Эта конфигурация позволит Docker выделить подсети в диапазонах 10.66.0.1-10.66.254.254 и 10.77.0.1-10.77.254.254.

И нажмите кнопку Apply & restart:

Так как Docker назначает адреса при создании сети, то нужно удалить существующие, создать заново и привязать её к контейнерам:

$ docker network rm dev_dev

$ docker network create dev_dev

$ docker network connect dev_dev dev-mysql-1
$ docker network connect dev_dev dev-redis-1
$ docker network connect dev_dev dev-redis-webui-1

Теперь мы видим что подсеть имеет адрес из нового пула:

$ docker network inspect dev_dev
[
    {
        "Name": "dev_dev",
        "Id": "be93ad5ca2e203d8c029d9b12966e957c042fbcab5c1e5ef2b1dcfbd1e966c41",
        "Created": "2023-07-12T09:48:15.913939494Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "10.66.1.0/24",
                    "Gateway": "10.66.1.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

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

Назначение адреса в docker-compose.yml

Также можно назначить адрес в файле docker-compose.yml:

version: '3'

services:
    mysql:
        build: mysql
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-dev}'
            MYSQL_ROOT_HOST: "%"
            MYSQL_DATABASE: '${DB_DATABASE:-default}'
            MYSQL_USER: '${DB_USERNAME:-dev}'
            MYSQL_PASSWORD: '${DB_PASSWORD:-dev}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
            TZ: 'UTC'
        volumes:
            - 'mysql:/var/lib/mysql'
            - './mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
            - './mysql/reset-users-passwords.sh:/docker-entrypoint-initdb.d/10-reset-users-passwords.sh'
        networks:
            - dev
        healthcheck:
            test: [ "CMD", "mysqladmin", "ping", "-p${DB_PASSWORD:-dev}" ]
            retries: 3
            timeout: 5s

    redis:
        image: 'redis:alpine'
        ports:
            - '${FORWARD_REDIS_PORT:-6379}:6379'
        networks:
            - dev
        healthcheck:
            test: [ "CMD", "redis-cli", "ping" ]
            retries: 3
            timeout: 5s

    redis-webui:
        image: 'erikdubbelboer/phpredisadmin:latest'
        environment:
            - 'REDIS_1_HOST=${REDIS_WEBUI_CONNECT_HOST:-redis}'
            - 'REDIS_1_PORT=${REDIS_WEBUI_CONNECT_PORT:-6379}'
        volumes:
            - './redis/includes/config.sample.inc.php:/src/app/includes/config.sample.inc.php'
        ports:
            - '${REDIS_WEBUI_PORT:-9987}:80'
        networks:
            - dev
        depends_on:
            - redis

networks:
    dev:
        driver: bridge
        ipam:
            config:
                - subnet: 10.66.0.0/16
                  gateway: 10.66.0.1

volumes:
    mysql:
        driver: local

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

$ docker network rm dev_dev

$ docker network create dev_dev --gateway=10.66.0.1 --subnet=10.66.0.0/16

$ docker network connect dev_dev dev-mysql-1
$ docker network connect dev_dev dev-redis-1
$ docker network connect dev_dev dev-redis-webui-1

Плюс этого метода в том, что конфигурация будет сохранена в окружении.

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

Заключение

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

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

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

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


  1. rionnagel
    12.07.2023 13:14
    -1

    Это болезнь такая, выделять сети на /16 и заводить там 3 пода?


    1. jingvar
      12.07.2023 13:14
      -1

      Больше контейнеров богу контейнеров.


    1. Helldar Автор
      12.07.2023 13:14
      -1

      У меня, например, подключены две конфигурации контейнеров через Docker Dev Environment и каждая автоматом создаёт свою подсеть при том, что в файле конфигурации указано одинаковое значение в обоих случаях:

      networks:
          dev:
              driver: bridge

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

      Да, с 16-й маской по количеству адресов перебор, но, во-первых, такой трюк позволит практически никогда не возвращаться к этим настройкам, а во-вторых, конфиг находится на локальной машине и не имеет никакого отношения к серверу. Так что, почему бы и да ¯\_(ツ)_/¯


      1. rionnagel
        12.07.2023 13:14

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


  1. alter72
    12.07.2023 13:14
    +1

    Проблема шире - в допущении что две сети должны быть достижимы без маршрутизации. Удаленная приватная докер сеть не должна бриджеваться в локальную


    1. Helldar Автор
      12.07.2023 13:14

      На тестовом стенде установлен докер-контейнер с приложениями и всем прочем, в числе которых postgres и redis, к которым любой разработчик и тестировщик имеют возможность подключиться напрямую со своего компа что позволяет сильно упростить процесс тестирования и дебага приложений перед релизом. Контейнеры под Docker Swarm крутятся.

      Кроме того, в нашем случае список занятых сервисами IP-адресов в сети чётко регламентированы девопсами.


      1. alter72
        12.07.2023 13:14

        Ну ок. Только эти сервисы должны быть доступны не по IP внутренней сети докера, а по IP хоста в локальной сети


        1. Helldar Автор
          12.07.2023 13:14

          Согласен. Но имеем то что имеем ¯\_(ツ)_/¯