Що таке EXPOSE в Dockerfile і чим відрізняється від публікації порту?
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, щоб використати їх з лептопа.
Швидкий приклад
FROM nginx:1.27-alpine
EXPOSE 80# Збираємо і запускаємо БЕЗ -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:
$ docker inspect mysite --format '{{json .Config.ExposedPorts}}'
{"80/tcp":{}}Ось і все. Метадані існують з двох причин:
- Документація: коли хтось робить
docker inspect, бачить, які порти image очікує. Корисно для авторів інструментів і людей, що читають image. - Флаг
-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:
- Обирає host-порт (8080) і container-порт (80).
- Додає iptables DNAT-правило, що форвардить
host:8080 -> container:80. - (На Linux) стартує
docker-proxyuserland-процес як резервний forwarder для IPv6 і edge-кейсів.
Порт 80 container тепер доступний з будь-де на host-мережі.
Варіанти синтаксису -p
-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
$ docker run -d -P nginx
$ docker port <container>
80/tcp -> 0.0.0.0:32768Docker обирає випадкові високі host-порти і мапить кожен EXPOSEd. Корисно у CI, коли байдуже, який host-порт; треба якийсь. Комбінується з docker port, щоб знайти присвоєний.
Типові помилки
Додати EXPOSE 8080 і чекати, що host його побачить
EXPOSE 8080$ docker run -d mysite
$ curl localhost:8080 # Connection refusedLише EXPOSE недостатньо. Все одно треба -p або -P під час запуску.
Забути EXPOSE і дивуватися, що -P нічого не робить
# Dockerfile без EXPOSE
FROM alpine:3.21
CMD ["nc", "-l", "-p", "8080"]$ docker run -d -P myimg
$ docker port <container>
# (порожньо, нема що публікувати)-P знає лише про EXPOSEd порти. Без EXPOSE 8080 нема що мапити.
Перевернути напрямок -p
# НЕПРАВИЛЬНО: думає, 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
Це частина, що людей дивує:
# 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, запущений двома способами
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "server.js"]# Без -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 # OKCompose з internal-only і опублікованими
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 патерн.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів