Skip to main content

Що таке 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, щоб використати їх з лептопа.

Швидкий приклад

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 патерн.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів