Мережа Docker Compose: DNS, host mode і порти
Як працює bridge-мережа Docker Compose, DNS за іменем сервісу, host mode, спільна мережа між проектами та чому published port обходить UFW.
Що Compose створює до запуску вашого застосунку
Мережа Docker Compose починається з одного правила: docker compose up створює приватну мережу для проекту, підключає до неї кожен сервіс і дає цим сервісам змогу звертатися один до одного за іменем сервісу. Для цього не потрібно писати жодного рядка networks:. Більшість непорозумінь щодо мережі Compose виникає через те, що користувачі не знають: мережа за замовчуванням уже створюється.
Ось невеликий файл. Збережіть його як compose.yaml у каталозі з назвою shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleЗапустіть його та перегляньте, що створив Docker:
docker compose up -d
docker network lsТепер у списку є мережа з назвою shop_default. Compose називає її <project>_default, а назвою проекту за замовчуванням є ім’я каталогу в нижньому регістрі. Перевизначте його за допомогою docker compose -p myproject up -d або за допомогою верхньорівневого name: myproject у файлі. Її драйвер — bridge, тобто віртуальний комутатор усередині хоста. Кожен контейнер отримує адресу в приватній підмережі, а вихідний трафік під час передавання назовні транслюється на адресу хоста.
docker compose down знову видаляє цю мережу. Тому застарілий контейнер зі старого проекту може не давати видалити мережу: Docker повертає error while removing network: network shop_default has active endpoints, а проблему усувають, зупинивши або видаливши контейнер, який усе ще підключений до неї.
Якщо Compose для вас новий, спочатку варто прочитати структуру файлу Compose і команди керування життєвим циклом, оскільки все нижче передбачає, що ви вмієте запускати й зупиняти проект.
DNS за іменем сервісу — це те, що початківці часто не враховують
У будь-якій мережі, визначеній користувачем, Docker запускає вбудований DNS-сервер, доступний кожному контейнеру за адресою 127.0.0.11. Він зіставляє імена сервісів із поточними адресами контейнерів. Тому web підключається до бази даних за іменем хоста db і через порт 5432 без будь-якої додаткової конфігурації.
docker compose exec web getent hosts dbКоманда виведе рядок на кшталт 172.18.0.2 db. Якщо вона нічого не виводить, два сервіси не підключені до однієї мережі.
Помилка, якої майже всі припускаються хоча б раз, — використання localhost у конфігурації застосунку. Усередині контейнера localhost означає цей контейнер, а не хост і не інший сервіс. Клієнти Postgres чітко повідомляють про це:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?Рядок підключення має бути таким: postgresql://postgres:example@db:5432/postgres. Частина з іменем хоста — це ім’я сервісу.
Є дві деталі, які згодом заощадять час. Імена зіставляються з тим, що запущено зараз, тому docker compose up -d --scale web=3 повертає одне ім’я з трьома адресами, а клієнт, який нескінченно кешує DNS, прив’яже себе до непрацюючого контейнера. Крім того, у застарілій мережі bridge, яку використовує звичайний docker run без --network, взагалі немає розпізнавання імен. Саме тому поради щодо зв’язків між контейнерами з 2016 року не відповідають тому, що ви бачите.
Вам не потрібен ports:, щоб з’єднати два сервіси
ports: публікує порт контейнера на хості. Він призначений для мережевого трафіку, що надходить із-за меж Docker. Він не має жодного стосунку до трафіку між сервісами, який і так працює через увесь діапазон портів у мережі проєкту.
Тому ports: - "5432:5432", який багато хто додає до сервісу бази даних, не дає користі й створює реальну проблему: він відкриває Postgres на загальнодоступному інтерфейсі сервера. Видаліть його. Якщо потрібно підключатися до нього з ноутбука для міграції, прив’яжіть його до loopback за допомогою "127.0.0.1:5432:5432" і підключайтеся через SSH-тунель. Різницю між сокетом, що прослуховується, опублікованим портом і правилом брандмауера описано в розділі як працюють порти та сервіси, що прослуховують з’єднання, у Linux.
expose: у Compose призначений лише для документації. Він нічого не відкриває, оскільки між контейнерами в одній мережі нічого не було закрито.
Коли режим network_mode host доречний і якою є його ціна
Режим host скасовує окремий мережевий простір імен контейнера та дає процесу прямий доступ до інтерфейсів хоста.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityДля цього є обґрунтовані причини. Процес, якому потрібно бачити broadcast- або multicast-трафік у локальній мережі, наприклад для виявлення пристроїв медіасервером або системою автоматизації будинку, не може бачити його за bridge, оскільки bridge не пересилає такий трафік до контейнера. Агент моніторингу, який читає лічильники інтерфейсів хоста, потребує доступу до інтерфейсів хоста. Також пропускається етап трансляції адрес, що важливо за високої інтенсивності пакетів.
Обмеження є конкретними.
ports: перестає працювати. Docker попереджає, що опубліковані порти ігноруються в режимі мережі host, а контейнер прив'язує ті порти, які використовує його процес. Два контейнери в режимі host, яким потрібен порт 8080, конфліктують, і другий завершується з помилкою bind: address already in use.
Розпізнавання за іменем сервісу не працює в обох напрямках. Контейнер не підключений до мережі проєкту, тому він не може розпізнати db, і інші сервіси також не можуть його розпізнати. Він може підключатися до них лише через порти, опубліковані на хості, зазвичай через 127.0.0.1.
Ізоляція відсутня. Процес, який прив'язує 0.0.0.0 всередині контейнера в режимі host, прослуховує всі інтерфейси вашого сервера, зокрема публічний, так само як пакет, інстальований за допомогою apt. Це має одну перевагу: такий трафік проходить стандартним шляхом вхідного трафіку, тому правила UFW застосовуються до нього. Для опублікованих портів це не так.
Режим host — це функція Linux Docker Engine. Docker Desktop підтримує його лише починаючи з версії 4.34 і лише після ввімкнення. Крім того, контейнери не можуть прив'язувати IP-адреси хоста, а підтримуються лише TCP і UDP. Якщо частина вашої команди працює на серверах Linux, а частина — у Docker Desktop, очікуйте, що той самий файл поводитиметься по-різному.
Використовуйте режим host, коли потрібен доступ до інтерфейсів хоста. Не використовуйте його для виправлення проблеми з підключенням, оскільки зазвичай він замінює одну проблему складнішою.
Підключення двох проєктів Compose до зовнішньої мережі
Мережа, створена одним проєктом, не видима для іншого. Тому reverse proxy у proxy/compose.yaml не бачить застосунок у app/compose.yaml, навіть якщо вони працюють на одному сервері. Рішення — мережа, якою не володіє жоден із проєктів.
Створіть її один раз вручну:
docker network create edgeПотім оголосіть її зовнішньою в кожному проєкті. Сторона proxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueСторона застосунку:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true вказує Compose підключитися до наявної мережі, а не створювати її, і залишити її в системі під час docker compose down. Окремий ключ name: важливіший, ніж може здатися: без нього Compose шукає мережу з назвою, яка точно відповідає edge, а з ним можна вказати для мережі одну назву у файлі, а іншу — на хості.
Якщо мережа не існує, Compose відмовляється запускатися та повідомляє, що мережу оголошено зовнішньою, але не знайдено. Спочатку створіть її.
Зверніть увагу, що файл застосунку робить із internal. База даних підключена лише до локальної мережі цього проєкту, тому proxy не може звертатися до неї, а це може робити лише app. Додавання internal: true до мережі повністю прибирає для неї маршрут у зовнішню мережу. Це оптимальний типовий варіант для бази даних, але перед налаштуванням слід врахувати один недолік: контейнер у внутрішній мережі не може нічого завантажувати, тому entrypoint, який під час запуску виконує apt-get update або pip install, зависне, а потім завершиться з помилкою через перевищення часу очікування.
Повний приклад налаштування з правилами маршрутизації та сертифікатами наведено в матеріалі запуск кількох застосунків за одним екземпляром Traefik.
Опубліковані порти обходять UFW
Це частина мережевої взаємодії Compose, яка може призвести до інциденту безпеки. Ви публікуєте порт, перевіряєте, що UFW активний і забороняє все, крім SSH, але служба все одно доступна з інтернету.
sudo ufw status
curl http://203.0.113.10:8080UFW повідомляє, що порт заблоковано. Але curl все одно повертає сторінку. Помилки немає. Docker записує власні правила трансляції адрес і пересилання безпосередньо в iptables. Трафік до опублікованого порту контейнера пересилається до контейнера, а не доставляється на хост. Тому він не проходить через ланцюжок, яким UFW керує для трафіку, призначеного локальному хосту. Правила Docker також перевіряються раніше за правила UFW.
Коротке рішення — публікувати порт лише там, де це потрібно:
ports:
- "127.0.0.1:8080:80"Це прив’язує порт на стороні хоста до loopback. Тому порт доступний із самого сервера та через SSH-тунель, але більше ніде. Розмістіть публічну точку входу за reverse proxy, який навмисно публікує порти 80 і 443. Повне пояснення, зокрема про ланцюжок DOCKER-USER для випадків, коли потрібно фільтрувати опублікований порт, наведено в розділі чому Docker публікує порти в обхід UFW і як це виправити.
Як налагодити це за допомогою чотирьох команд
Спочатку перевірте, до якої мережі фактично підключено кожен контейнер:
docker network inspect shop_defaultУ блоці Containers перелічено всі підключені контейнери та їхні адреси. Сервіс, якого немає в цьому списку, підключений до іншої мережі, працює в режимі host або не запущений.
Перевірте розпізнавання імен із тимчасового контейнера, підключеного до тієї самої мережі. Так вам не потрібно встановлювати додаткові інструменти у власні образи:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Помилка nslookup вказує на проблему з розпізнаванням імен або належністю до мережі. Якщо nslookup виконується успішно, а nc завершується помилкою, це означає, що сервіс запущений, але не прослуховує цей порт або прослуховує 127.0.0.1 всередині власного контейнера замість 0.0.0.0. Це часто трапляється з серверами розробки. Виправлення потрібно внести в адресу прив’язки застосунку, а не в Docker.
Є ще одна проблема, яка виглядає як помилка Docker. Якщо контейнери можуть обмінюватися даними між собою, але не можуть підключитися до комп’ютера в офісній мережі або VPN, імовірно, підмережа Docker перетинається з цією мережею. За замовчуванням Docker виділяє підмережі, починаючи з 172.17.0.0/16. Змініть пул у /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Потім виконайте sudo systemctl restart docker і повторно створіть відповідні мережі, оскільки наявна мережа зберігає підмережу, з якою її було створено.
FAQ
Чому мої контейнери не можуть звертатися один до одного за іменем сервісу?
Вони не підключені до тієї самої мережі. Compose автоматично підключає кожен сервіс до <project>_default, але після додавання списку networks: до сервісу цей список стає повним набором його мереж, і мережа за замовчуванням більше не додається неявно. Виконайте docker network inspect <network> і перевірте, чи обидва контейнери відображаються в блоці Containers. Також перевірте, чи жоден сервіс не використовує network_mode: host, оскільки контейнер у режимі host не підключений до мережі Docker і не може визначати імена сервісів.
Чи потрібно публікувати порти, щоб один сервіс міг звертатися до іншого?
Ні. У мережі Compose кожен порт кожного контейнера доступний іншим контейнерам у цій мережі. ports: потрібен лише для відкриття контейнера для трафіку ззовні Docker, а expose: слугує для документації. Публікація порту бази даних є поширеною та небезпечною практикою, оскільки вона розміщує базу даних у загальнодоступному інтерфейсі сервера.
Яка різниця між мережами bridge і host?
bridge надає контейнеру власний мережевий простір і адресу у віртуальному комутаторі, автоматичне визначення імен між контейнерами та трансляцію вихідного трафіку. host безпосередньо використовує мережевий стек хоста: окремої адреси немає, визначення за іменем сервісу немає, публікація портів не потрібна, а ізоляція від інших слухачів хоста відсутня. bridge є режимом за замовчуванням і правильним вибором, якщо процесу не потрібен доступ до інтерфейсів хоста.
Як підключити контейнери з двох різних файлів Compose?
Створіть спільну мережу за допомогою docker network create edge, потім оголосіть її в обох файлах за допомогою external: true і підключіть до неї сервіси, яким потрібно обмінюватися даними. Compose не створюватиме і не видалятиме цю мережу. Якщо пропустити крок створення, Compose відмовиться запускати проєкт і повідомить, що оголошену зовнішню мережу не знайдено.
Чому мій контейнер доступний з Інтернету, коли UFW блокує порт?
Тому що опублікований порт обробляється правилами переспрямування, які Docker додає до iptables. Ці правила перевіряються раніше за правила UFW, а переспрямований трафік у будь-якому разі не проходить через ланцюжок, який фільтрує UFW. Прив’яжіть сторону хоста до loopback за допомогою "127.0.0.1:8080:80" і розмістіть усі загальнодоступні сервіси за reverse proxy на портах 80 і 443.