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.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ