Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке EXPOSE в Dockerfile і чим відрізняється від публікації порту?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`EXPOSE`** у Dockerfile це лише документація, повідомляє людям (і `docker run -P`), який порт image слухає. НІЧОГО не публікує. **`-p` / `--publish`** на `docker run` це те, що реально мапить host-порт на container-порт. ```dockerfile EXPOSE 80 # документація; container ще НЕ доступний з host ``` ```bash docker run nginx # EXPOSE 80, але НЕ доступний з host docker run -p 8080:80 nginx # ТЕПЕР доступний на http://localhost:8080 docker run -P nginx # публікувати всі EXPOSEd порти на випадкових host-портах ``` **Головне:** EXPOSE це підказка, не дія. Порт container завжди доступний з інших container на тій самій мережі; `-p` додає доступ з host-машини.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`EXPOSE` і публікація порту** виглядають пов'язаними, але роблять зовсім різні речі. EXPOSE це документація; публікація (`-p`) це те, що реально робить порт доступним з host. Плутанина між ними це один з найпоширеніших Docker-багів у dev-середовищах. ## Теорія ### TL;DR - `EXPOSE 80` у Dockerfile = **лише метадані**. Записує, що image слухає порт 80. Жодної дірки у firewall, жодного NAT-правила. - `-p HOST:CONTAINER` на `docker run` = **реально мапить порт**. Додає iptables NAT-правило. - `-P` (велика) на `docker run` = **публікує кожен EXPOSEd порт** на випадкові host-порти. EXPOSE важливий лише через цей флаг. - Без `-p` порт container доступний з інших container на тій самій Docker-мережі, але НЕ з host чи зовнішнього світу. - Більшість image (`nginx`, `postgres`) мають EXPOSE для документації. Все одно потрібен `-p`, щоб використати їх з лептопа. ### Швидкий приклад ```dockerfile FROM nginx:1.27-alpine EXPOSE 80 ``` ```bash # Збираємо і запускаємо БЕЗ -p $ docker build -t mysite . $ docker run -d --name web mysite $ curl http://localhost:80 curl: (7) Failed to connect to localhost port 80 # Порт 80 container піднятий внутрішньо; нічого не мапить його на host. # Тепер З -p $ docker rm -f web $ docker run -d --name web -p 8080:80 mysite $ curl http://localhost:8080 <html>...</html> # ← працює ``` EXPOSE був ідентичний в обох запусках. Лише `-p` зробив порт 80 видимим для host. ### Що EXPOSE насправді робить EXPOSE пише один шматок метаданих у config image: ```bash $ docker inspect mysite --format '{{json .Config.ExposedPorts}}' {"80/tcp":{}} ``` Ось і все. Метадані існують з двох причин: 1. **Документація:** коли хтось робить `docker inspect`, бачить, які порти image очікує. Корисно для авторів інструментів і людей, що читають image. 2. **Флаг `-P`:** `docker run -P` (велика P) публікує кожен EXPOSEd порт на випадковий host-порт. Без EXPOSE `-P` нічого публікувати. Воно НЕ: - Відкриває жодного host-порту - Конфігурує iptables / NAT - Робить container доступним поза Docker-мережею - Впливає на трафік між container на Docker-мережах (container завжди можуть говорити один з одним на будь-якому порту, якщо на тій самій мережі) ### Що `-p` насправді робить Коли робиш `docker run -p 8080:80 nginx`, daemon: 1. Обирає host-порт (8080) і container-порт (80). 2. Додає iptables DNAT-правило, що форвардить `host:8080 -> container:80`. 3. (На Linux) стартує `docker-proxy` userland-процес як резервний forwarder для IPv6 і edge-кейсів. Порт 80 container тепер доступний з будь-де на host-мережі. ### Варіанти синтаксису `-p` ```bash -p 8080:80 # host 8080 → container 80, всі інтерфейси -p 127.0.0.1:8080:80 # тільки loopback (localhost-only) -p 80 # випадковий host-порт → container 80 -p 8080:80/udp # UDP замість TCP -p 8080-8090:80-90 # range mapping ``` Повна форма: `[HOST_IP:]HOST_PORT:CONTAINER_PORT[/PROTOCOL]`. ### `-P` (велика): publish-all ```bash $ docker run -d -P nginx $ docker port <container> 80/tcp -> 0.0.0.0:32768 ``` Docker обирає випадкові високі host-порти і мапить кожен EXPOSEd. Корисно у CI, коли байдуже, який host-порт; треба *якийсь*. Комбінується з `docker port`, щоб знайти присвоєний. ### Типові помилки **Додати `EXPOSE 8080` і чекати, що host його побачить** ```dockerfile EXPOSE 8080 ``` ```bash $ docker run -d mysite $ curl localhost:8080 # Connection refused ``` Lише EXPOSE недостатньо. Все одно треба `-p` або `-P` під час запуску. **Забути EXPOSE і дивуватися, що `-P` нічого не робить** ```dockerfile # Dockerfile без EXPOSE FROM alpine:3.21 CMD ["nc", "-l", "-p", "8080"] ``` ```bash $ docker run -d -P myimg $ docker port <container> # (порожньо, нема що публікувати) ``` `-P` знає лише про EXPOSEd порти. Без `EXPOSE 8080` нема що мапити. **Перевернути напрямок `-p`** ```bash # НЕПРАВИЛЬНО: думає, 80 це host, 8080 це container $ docker run -p 80:8080 nginx # Container слухає 80 (його реальний порт). Нічого на 8080. # Host:80 повертає connection refused. # ПРАВИЛЬНО: HOST_PORT:CONTAINER_PORT $ docker run -p 80:80 nginx ``` Класичний gotcha. Порядок: host перший, container другий. **Використання EXPOSE для безпеки** EXPOSE нічого не обмежує. Реальні listening-порти container приходять з того, на що bind'ить застосунок всередині, не з EXPOSE. Застосунок, що bind'иться на 22 всередині container з `EXPOSE 80`, все одно слухає 22, і інші container на тій самій мережі його дістануть. ### Спілкування між container не потребує EXPOSE або `-p` Це частина, що людей дивує: ```yaml # compose.yaml services: api: image: myapp # без ports: опубліковано db: image: postgres:16 # без ports: опубліковано ``` З container `api` `db:5432` чудово працює. Bridge-мережа, що створив Compose, дозволяє container говорити один з одним на будь-якому порту, який слухає destination. EXPOSE і `-p` тільки про HOST-видимість. Хороша security-практика: НЕ публікуй DB-порти на host у проді. Публікуй лише те, до чого має бути доступ зовні (web, API), а внутрішні сервіси хай говорять через Docker-мережу. ### Реальне застосування - **Публічні сервіси:** `-p 80:80 -p 443:443` на reverse proxy / веб-сервері. - **Внутрішні сервіси (DB, cache, queue):** взагалі без `-p` у проді. Інші container їх дістають через Docker DNS по service-імені. - **CI-тести:** `-P`, щоб ухопити будь-який вільний host-порт, потім `docker port`, щоб знайти. Корисно при паралельних тестових інстансах одного image, що не можуть ділити host-порти. - **EXPOSE у Dockerfile:** тримай для документації. Хто читає Dockerfile, знає порт застосунку без запуску. ### Питання для поглиблення **Q:** Яка різниця між `EXPOSE` у Dockerfile і `expose:` у Compose? **A:** Та сама ідея, трохи інший scope. Dockerfile `EXPOSE` стає частиною image. Compose `expose:` застосовується лише під час run і до інших Compose-сервісів на тій самій мережі, все ще без публікації на host. Обидва документація/метадані. Бери `ports:` у Compose, щоб реально публікувати. **Q:** Чому `docker port` показує два рядки для одного порту? **A:** IPv4 і IPv6: `0.0.0.0:8080` і `[::]:8080`. Дві address-сім'ї, один логічний mapping. **Q:** Чи можу опублікувати порт ПІСЛЯ запуску container? **A:** Ні (зі стандартним CLI). Маєш зупинити, перестворити з `-p` і знов стартувати. Compose робить це менш болючим, `docker compose up -d` помічає зміну і перестворює лише той сервіс. **Q:** Яка різниця між bind на `0.0.0.0` і `127.0.0.1`? **A:** `0.0.0.0` = всі host-інтерфейси (публічно доступний на будь-якій мережі, де host). `127.0.0.1` = тільки loopback (тільки ця машина). Для dev-tools, що мають бути доступні лише локально, бери `127.0.0.1:8080:80`. **Q:** (Senior) Як захистити публічно опублікований порт у Docker? **A:** Container і `-p` дають iptables DNAT-правила, що за замовчуванням оминають UFW/firewalld, Docker пише свої правила у DOCKER-chain, що крутиться перед INPUT. Щоб обмежити, або bind'ся на `127.0.0.1:8080:80` (тільки локально), або використовуй `iptables-save` правила у DOCKER-USER chain для фільтрації, або став перед container справжній reverse proxy + cloud firewall замість того, щоб довіряти `-p` для безпеки. ## Приклади ### Image з EXPOSE, запущений двома способами ```dockerfile FROM node:22-alpine WORKDIR /app COPY . . RUN npm ci --omit=dev EXPOSE 3000 CMD ["node", "server.js"] ``` ```bash # Без -p: застосунок крутиться, але недоступний з host $ docker run -d --name app myapp $ curl localhost:3000 # connection refused # З явним -p: доступний $ docker run -d -p 3000:3000 myapp $ curl localhost:3000 # OK # Або з -P (випадковий host-порт) $ docker run -d -P myapp $ docker port <id> 3000/tcp -> 0.0.0.0:32789 $ curl localhost:32789 # OK ``` ### Compose з internal-only і опублікованими ```yaml services: web: image: nginx ports: - "80:80" # публічно api: image: myapp expose: - "3000" # тільки внутрішньо, web дістає api:3000, host не може db: image: postgres:16 # без ports / без expose, лише api дістає db:5432 ``` Три сервіси, лише один (web) доступний з host. Інші два ізольовані всередині Docker-мережі project. Це production-shape патерн.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.