JavaRush /Курси /Docker for Spring /Порти в Docker: сервіс не відкривається

Порти в Docker: сервіс не відкривається

Docker for Spring
Рівень 2 , Лекція 4
Відкрита

1. Симптом: контейнер запущений, а HTTP «не відкривається»

Якщо контейнер запущений, а HTTP із хоста не відкривається, найчастіше проблема в портах і адресах, а не в «зламаному Docker». Контейнер може виглядати живим: він є в docker ps, процеси всередині на місці, а під час спроби відкрити сервіс із хоста ви отримуєте Connection refused або нескінченне очікування. Новачок у цей момент зазвичай підозрює все — від «Docker зламався» до «в мене проклятий ноутбук». Але на практиці помилка найчастіше набагато прозаїчніша: ви дивитеся не туди — не на той порт, не на ту адресу або взагалі забули «показати» порт назовні.

Важливо зафіксувати головне правило дня: контейнер може бути живим, а сервіс при цьому може бути недоступним із вашої машини. І це не суперечність. Контейнер — окремий мережевий «світ», а доступ із хоста — окреме налаштування, яке потрібно зробити явно.

Саме тому порти варто розбирати окремо. Той самий симптом трапляється і в готового httpd, і у власного бекенд-сервісу: образ змінюється, але питання лишаються ті самі — чи опублікований порт, до якого порту хоста ви звертаєтеся і на якій адресі взагалі слухає процес.

2. Два різні порти: «всередині контейнера» і «на вашій машині»

Коли ми говоримо «сервіс слухає порт 80» або «Spring Boot слухає 8080», важливо уточнювати, у якому світі це відбувається. Docker якраз і створює два світи: світ вашого комп’ютера (host) і світ контейнера. Номери портів у них можуть збігатися, але це різні двері в різних будівлях. Сказати просто «8080» і не уточнити, де саме, — уже добрий спосіб влаштувати собі пригоди.

Подивімося на це як на просту схему. Користувач (ви) відкриває localhost:8080 на хост-машині. Docker може пробросити цей запит усередину контейнера, наприклад на порт 80, де слухає вебсервер. Якщо пробросу немає, запит просто не потрапить у контейнер, навіть якщо всередині все чудово працює.

flowchart LR
    %% Ідея схеми: ліворуч — ви (host), посередині — публікація порту, праворуч — контейнер і процес
    U["Ви: браузер / curl
на хост-машині"] -->|"localhost:8080"| P["Публікація порту
docker run -p"] P -->|"контейнер:80"| C["Контейнер"] C --> S["Процес усередині контейнера
(наприклад, httpd)"]

Ключова думка: порт контейнера — це порт процесу всередині контейнера. Порт хоста — це порт на вашій машині, куди ви звертаєтеся через localhost:.... І команда docker run -p — це «перекладач» між ними.

3. docker run -p і формат HOST:CONTAINER

Прапорець -p у docker run — це і є публікація порту. Він каже Docker: «зроби так, щоб те, що відбувається на порті контейнера, стало доступним зовні на порту моєї машини». Важливо сприймати це не як «ще один прапорець», а як частину мережевого контракту: без нього ваш сервіс майже завжди лишається «внутрішнім».

Найбазовіша форма виглядає так:

Запис у docker run Як читати людською мовою
-p 8080:80 «Відкрий на host порт 8080 і спрямовуй трафік у контейнер на порт 80»

Давайте подивимося на мінімальний приклад із готовим вебсервером httpd (Apache). Він зручний тим, що одразу вміє відповідати по HTTP і не вимагає від нас жодного коду.

# Запускаємо контейнер у тлі (-d) і публікуємо порт: 8080 на host -> 80 у контейнері
docker run -d --name web8080 -p 8080:80 httpd:2.4

# Перевіряємо з host-машини, що HTTP справді доступний на опублікованому порту
curl -I localhost:8080

# Очікуємо побачити заголовки відповіді та код 200, отже публікація порту працює
# HTTP/1.1 200 OK

Тут відбувається корисна «магія без магії». Усередині контейнера httpd слухає порт 80. Ззовні ви звертаєтеся до localhost:8080. І Docker робить міст: 8080 на host → 80 у контейнері.

Якщо ви раптом переплутаєте порядок і напишете -p 80:8080, Docker чесно пробросить порт 80 хоста в порт 8080 контейнера... але в контейнері на 8080 нічого не слухає. У результаті контейнер буде «живий», а HTTP для вас — «мертвий». Саме тому ми так наполегливо проговорюємо: ліворуч — host, праворуч — container.

Ще один важливий момент: якщо host-порт уже зайнятий (наприклад, ви вибрали 8080, а там у вас локально вже працює інший сервіс), Docker не зможе «підселитися» в той самий порт. У цьому разі docker run зазвичай завершується помилкою на кшталт “port is already allocated”. Це не баг Docker, а нормальна фізика: два різні процеси на одному host-порті без спеціальних трюків не уживаються.

Перевірка публікації: docker ps і docker inspect

Коли сервіс «не відкривається», дуже легко зірватися в режим героя бойовика: перезапускати все підряд, лізти в конфіги й загалом рухатися швидко, але без плану. На практиці найчастіше достатньо спочатку подивитися на факти, і Docker дає для цього два зручні джерела: docker ps і docker inspect. Перше — швидкий огляд, друге — структуроване джерело правди.

Почнімо з docker ps. У контейнера є колонка PORTS. Вона спеціально існує, щоб ви відразу бачили: чи опублікований порт назовні, чи ні.

# Дивимося список контейнерів і колонку PORTS (там і живе вся правда про публікацію портів)
docker ps

# CONTAINER ID   NAMES     STATUS         PORTS
# ...            web8080   Up ...         0.0.0.0:8080->80/tcp

Запис 0.0.0.0:8080->80/tcp читається так: «на host-машині відкритий порт 8080 (на всіх інтерфейсах), і він спрямовується в контейнер на порт 80 по TCP». Навіть якщо ви поки не дуже розумієте 0.0.0.0, уже видно головне: 8080->80.

Тепер подивімося на docker inspect — це вже докладна «анкета» контейнера. Повний inspect величезний, тому беремо рівно потрібну частину форматованим виводом: мапінг портів.

# Дістаємо з inspect тільки секцію з публікацією портів (у JSON), щоб не потонути в повному виводі
docker inspect --format '{{json .NetworkSettings.Ports}}' web8080

# Тут видно: контейнерний 80/tcp опубліковано як HostPort 8080 на HostIp 0.0.0.0
# {"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

Це той самий факт, але у вигляді JSON. Тут зручно бачити два поля: HostIp і HostPort. Ми ще повернемося до HostIp, коли будемо обговорювати 127.0.0.1 і 0.0.0.0.

Тепер корисно побачити контраст: контейнер запущений, сервіс усередині є, але порт назовні не опублікований. Це й є той самий сценарій «контейнер живий, але не відкривається».

# Запускаємо httpd БЕЗ публікації порту (-p немає) — зовні він буде недоступний
docker run -d --name web-no-port httpd:2.4

# У PORTS буде зазначений лише внутрішній порт контейнера, без стрілочки на host
docker ps
# ... web-no-port  Up ...   80/tcp

# На host перевіряти нічого: ми нічого не публікували, тому буде відмова в з’єднанні
curl -I localhost:8080
# curl: (7) Failed to connect to localhost port 8080: Connection refused

Зверніть увагу на деталь: у PORTS у web-no-port буде щось на кшталт 80/tcp, але без стрілочки -> на host. Це й є підказка: «порт 80 існує всередині контейнера», але його не опубліковано. Тобто Docker знає, що всередині є 80/tcp, але назовні його не показав.

І так, це ще одне важливе правило: docker ps показує, що контейнер «Up», але це не обіцянка, що ваш сервіс доступний із хоста. Це лише підтвердження, що основний процес контейнера живий. Доступність — окрема історія.

4. localhost та інші адреси

У Docker-контексті адреси швидко починають виглядати як заклинання з книжки початківця мережевого мага: localhost, 127.0.0.1, 0.0.0.0. І дуже часто проблема «не відкривається» — це не порт, а саме неправильне трактування адреси. Тут важливо не «вивчити магію», а просто зрозуміти сенс: де ми слухаємо і звідки звертаємося.

Спершу коротка таблиця, яку корисно тримати в голові:

Адреса Як сприймати
localhost «ця сама машина», зазвичай синонім 127.0.0.1 для перевірки в браузері/curl
127.0.0.1 loopback-інтерфейс: «тільки локально на цій машині»
0.0.0.0 «слухати на всіх інтерфейсах» (це не адреса, яку ви зазвичай пишете в URL)

Головна пастка: 0.0.0.0 ви часто бачите в docker ps або docker inspect, але не потрібно намагатися відкривати http://0.0.0.0:8080 у браузері як «правильну адресу». У більшості локальних сценаріїв ви відкриваєте http://localhost:8080 або http://127.0.0.1:8080.

Але для доступності з host-машини цього все одно недостатньо. Мають одночасно виконуватися дві умови:

- Docker публікує потрібний порт назовні через -p;
- процес усередині контейнера слухає не лише свій loopback 127.0.0.1, а доступний інтерфейс контейнера, зазвичай 0.0.0.0.

І тут легко переплутати дві різні локальності. 127.0.0.1 на host і 127.0.0.1 усередині контейнера — це не одна й та сама адреса. Якщо застосунок усередині контейнера прив’язаний лише до свого loopback, опублікований порт сам по собі не врятує: запит дійде до контейнера, але не до процесу.

Тепер подивімося на важливий практичний варіант публікації порту: можна «відкрити всім» (за замовчуванням), а можна «тільки локально». Це особливо корисно, коли ви не хочете світити сервісом у локальній мережі.

# Публікуємо порт так, щоб він слухав лише на loopback: доступний тільки з цієї машини
docker run -d --name web8090 -p 127.0.0.1:8090:80 httpd:2.4

# Перевіряємо, що Docker справді прив’язав публікацію до 127.0.0.1, а не до всіх інтерфейсів
docker inspect --format '{{json .NetworkSettings.Ports}}' web8090
# {"80/tcp":[{"HostIp":"127.0.0.1","HostPort":"8090"}]}

# Локальна перевірка пройде успішно
curl -I localhost:8090
# HTTP/1.1 200 OK

Локально все працює. Але якщо ви спробуєте відкрити цей порт не з цієї машини, а, наприклад, із телефона в тій самій мережі (або з ноутбука колеги), нічого не вийде. І це очікувано: ви самі попросили Docker слухати лише 127.0.0.1, тобто тільки loopback.

І ще одна плутанина, яка часто спливає саме в контейнерах: localhost усередині контейнера — це «сам контейнер», а localhost на host-машині — це «ваш комп’ютер». Це два різні «я вдома». Тут нас цікавить доступ із host-машини, бо саме на цій межі найчастіше ламається локальна перевірка.

5. Три часті поломки та діагностика командами

Замість того щоб запам’ятовувати десятки прапорців, набагато корисніше тримати в голові кілька характерних сценаріїв. У кожного — свої симптоми, і кожен діагностується буквально парою команд. Це як у медицині, тільки без білого халата: спершу вимірюємо тиск (ps/inspect), потім читаємо аналізи (logs), і лише потім робимо висновки.

Сценарій 1: порт узагалі не опублікований (забули -p).
Контейнер «Up», у docker ps ви бачите 80/tcp (або інший порт), але стрілочки -> на host немає. Із хоста будь-який curl localhost:... за очікуваним портом буде марним. Це лікується не «налаштуванням curl», а перезапуском контейнера з правильною публікацією порту.

# Запуск без -p: Docker не створює "вхідні двері" з host усередину контейнера
docker run -d --name web-no-port httpd:2.4

# У PORTS знову буде лише внутрішній порт контейнера (без публікації)
docker ps
# ... web-no-port  Up ...  80/tcp

Сценарій 2: порт опублікований, але ви перевіряєте не той host-порт.
Це найприкріша поломка, бо вона зазвичай виглядає як «усе зламалося», а насправді ви просто набрали «8091» замість «8090». У такий момент корисно не сперечатися з реальністю, а знову подивитися docker ps або inspect і підставити в curl саме ті цифри, які там написані.

# Публікуємо 8080 на host -> 80 у контейнері
docker run -d --name web8080 -p 8080:80 httpd:2.4

# Помилка перевірки: звертаємося до іншого порту, який не публікували
curl -I localhost:8090
# curl: (7) Failed to connect ...

# Правильна перевірка: використовуємо саме той host-порт, який указали в -p
curl -I localhost:8080
# HTTP/1.1 200 OK

Сценарій 3: ви «звузили» публікацію до 127.0.0.1, а потім чекаєте, що сервіс буде доступний «зовні».
З хоста (curl localhost:...) усе працюватиме, і це навіть підло: здається, що «мережа нормальна». Але щойно перевірка йде не з тієї самої машини, раптом «не відкривається». У такій ситуації docker inspect дуже швидко пояснює причину: HostIp буде 127.0.0.1. І це не помилка, а ваше явне налаштування.

# Обмежуємо публікацію до loopback — доступ буде лише з поточної машини
docker run -d --name web-local -p 127.0.0.1:8090:80 httpd:2.4

# HostIp покаже 127.0.0.1 — це й є причина, чому "із мережі" не достукатися
docker inspect --format '{{json .NetworkSettings.Ports}}' web-local
# {"80/tcp":[{"HostIp":"127.0.0.1","HostPort":"8090"}]}

Якщо ви дивитеся на ці три історії й думаєте «це занадто просто, щоб бути правдою», то вітаю: ви на правильному шляху. Більшість ранніх Docker-проблем вирішуються саме такими простими перевірками. Просто мозок після кількох років розробки звик, що «якщо не працює, значить проблема складна». Docker на перших кроках часто чесніший.

6. Міні-алгоритм діагностики

Дуже хочеться мати «одну команду, яка все полагодить». Але нормальна інженерія починається там, де замість магії є короткий, повторюваний порядок дій. Він не має бути довгим — інакше ви просто ним не користуватиметеся. Нехай буде так: п’ять хвилин на факти, і лише потім — на гіпотези.

Нижче — простий потік, який можна тримати в голові. Він спирається лише на команди, які ми вже маємо сьогодні.

flowchart TD
    %% Діагностика зверху вниз: контейнер живий? порт опублікований? правильний host-порт?
    A["Сервіс не відкривається з host"] --> B["docker ps: контейнер Up"]
    B -->|ні| C["Контейнер не працює: дивитися logs/exit code"]
    B -->|так| D["docker ps: чи є PORTS зі стрілочкою ->"]
    D -->|ні| E["Немає публікації: перезапуск з -p"]
    D -->|так| F["docker inspect: який HostPort?"]
    F --> G["curl localhost:HostPort"]
    G -->|не працює| H["docker logs (чи є помилки запуску?)"]
    G -->|працює| I["Ок: доступність підтверджено"]

Сенс цього алгоритму в тому, що він послідовно відповідає на кілька різних запитань. Спершу: «контейнер узагалі живий?» Потім: «порт узагалі опублікований?» І лише потім: «я правильно перевіряю host-порт?» Якщо на цих кроках усе виглядає правильно, а відповіді все одно немає, лишається ще одна часта гіпотеза: процес усередині контейнера слухає тільки свій loopback. Тоді проблема вже не в -p, а в адресі прив’язки самого процесу. Це сильно економить час і захищає від діагностики «на емоціях».

7. Типові помилки під час публікації портів

Помилка №1: вважати, що “порт контейнера” і “порт на комп’ютері” — це одне й те саме.
Це класика. У голові звучить думка: «Сервіс на 80 порті, отже відкриваю localhost:80». Але якщо ви публікували -p 8080:80, то з хоста ваш вхід — це 8080. У таких ситуаціях допомагає проста звичка: завжди дивитися на стрілочку HOST->CONTAINER у docker ps і буквально читати її вголос, як школяр на уроці.

Помилка №2: бачити 80/tcp у docker ps і думати, що “порт відкритий назовні”.
Запис 80/tcp без стрілочки — це, по суті, «внутрішній порт контейнера». Він може існувати, але бути недоступним ззовні. Docker тут не знущається з вас, він просто чесно показує дві різні речі: «у контейнері є такий порт» і «він опублікований». Не плутайте.

Помилка №3: намагатися відкривати http://0.0.0.0:8080 як “правильну адресу”.
0.0.0.0 — це не «адреса сервісу для браузера», а «слухати на всіх інтерфейсах» (те, що в inspect записано як HostIp). Для перевірки з цієї ж машини використовуйте localhost або 127.0.0.1. Якщо дуже хочеться перевірити «по-дорослому», можна використовувати реальну IP-адресу вашого комп’ютера, але це вже окрема історія.

Помилка №4: перевіряти не той host-порт (людський фактор, який перемагає навіть добрі мізки).
Зазвичай це виглядає так: «у мене все налаштовано, але не відкривається». Потім виявляється, що ви запустили на 8080, а перевіряєте 8090. Лікується це не знанням Docker, а дисципліною: docker ps → копіюємо порт → curl. Можна навіть не розуміти мережі — просто бути чесним із цифрами.

Помилка №5: випадково обмежити публікацію до 127.0.0.1 і чекати доступності «звідусіль».
Часто це спливає, коли людина десь побачила приклад 127.0.0.1:... і скопіювала його «про всяк випадок». Для локальної розробки це нормально і навіть корисно. Але якщо ви тестуєте доступ не з тієї самої машини (або в складніших середовищах), це виглядатиме як «Docker зламав мережу». Насправді Docker просто виконав прохання: «тільки локально». У inspect це видно відразу по HostIp.

1
Опитування
Основи Docker, рівень 2, лекція 4
Недоступний
Основи Docker
Команди образів і контейнерів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ