رفتن به محتوا
LoopX

پورت‌ها

توی این درس یاد می‌گیری چرا یک سرویس داخل کانتینر به‌صورت پیش‌فرض از بیرون در دسترس نیست و چطور با publish پورت (-p) آن را باز می‌کنی. فرق پورت میزبان (host) و پورت کانتینر را می‌فهمی، با -p 127.0.0.1:8080:80 پورت را فقط روی خود سیستم باز می‌کنی، با -P پورت‌های تصادفی می‌گیری و با docker port نقشه‌ی پورت‌ها را می‌خوانی. پرتکرارترین خطای داکر، «port is already allocated»، را هم می‌شناسی.

تشبیه: شماره‌ی داخلی و خط شهری

Section titled “تشبیه: شماره‌ی داخلی و خط شهری”

فرض کن یک شرکت (کانتینر) داری که تلفن داخلی ۸۰ آن وب‌سرور است. تلفن داخلی ۸۰ از بیرون شرکت قابل شماره‌گیری نیست. باید در اپراتور (میزبان) تنظیم کنی: «هر تماس با خط شهری ۸۰۸۰ را به داخلی ۸۰ وصل کن.» این همان publish پورت است: -p 8080:80 یعنی «خط شهری ۸۰۸۰ ← داخلی ۸۰».

مرورگر به پورت میزبان وصل می‌شود؛ داکر آن را به پورت داخل کانتینر هدایت می‌کند. بدون -p این پل وجود ندارد.
Terminal window
docker run -d --name lx-p1 nginx:alpine >/dev/null
sleep 2
echo "بدون -p:"; curl -s -o /dev/null -m 3 -w " HTTP %{http_code}\n" localhost:8320 || echo " در دسترس نیست"
docker rm -f lx-p1 >/dev/null
docker run -d --name lx-p1 -p 8320:80 nginx:alpine >/dev/null
sleep 2
echo "با -p 8320:80:"; curl -s -o /dev/null -w " HTTP %{http_code}\n" localhost:8320
خروجی
بدون -p:
HTTP 000
در دسترس نیست
با -p 8320:80:
HTTP 200

بدون -p، پورت ۸۰ فقط داخل شبکه‌ی داکر وجود دارد و از میزبان قابل دسترسی نیست. با -p 8320:80 (پورت-میزبان:پورت-کانتینر) داکر درخواست‌های پورت ۸۳۲۰ سیستم تو را به پورت ۸۰ کانتینر می‌فرستد.

مثال ۲: دو کانتینر، دو پورت

Section titled “مثال ۲: دو کانتینر، دو پورت”
Terminal window
docker run -d --name lx-p2 -p 8321:80 nginx:alpine >/dev/null
sleep 2
for p in 8320 8321; do curl -s -o /dev/null -w "پورت $p: HTTP %{http_code}\n" localhost:$p; done
خروجی
پورت 8320: HTTP 200
پورت 8321: HTTP 200

هر دو داخل خودشان روی پورت ۸۰ گوش می‌دهند و تداخلی ندارند، چون هر کانتینر شبکه‌ی خودش را دارد؛ فقط پورت‌های میزبان باید فرق کنند. این از بزرگ‌ترین مزیت‌های کانتینر است: همه می‌توانند روی پورت ۸۰ خودشان بنشینند.

مثال ۳: محدود کردن به localhost

Section titled “مثال ۳: محدود کردن به localhost”
Terminal window
docker run -d --name lx-p3 -p 127.0.0.1:8322:80 nginx:alpine >/dev/null
sleep 2
docker port lx-p1
docker port lx-p3
خروجی
80/tcp -> 0.0.0.0:8320
80/tcp -> [::]:8320
80/tcp -> 127.0.0.1:8322

-p 8320:80 به‌طور پیش‌فرض روی همه‌ی آدرس‌های میزبان (0.0.0.0) باز می‌شود؛ یعنی هر دستگاهی در شبکه‌ی تو می‌تواند به آن برسد. -p 127.0.0.1:8322:80 فقط روی خود سیستم باز می‌کند. برای دیتابیس‌ها و ابزارهای داخلی همیشه این شکل را بنویس.

⚡ بررسی سریع

کدام دستور پورت را فقط روی خود سیستم باز می‌کند؟

مثال ۴: پورت تصادفی با -P

Section titled “مثال ۴: پورت تصادفی با -P”
Terminal window
docker run -d --name lx-p4 -P nginx:alpine >/dev/null
sleep 2
docker port lx-p4
docker inspect lx-p4 --format 'EXPOSE شده: {{.Config.ExposedPorts}}'
خروجی
80/tcp -> 0.0.0.0:55001
EXPOSE شده: map[80/tcp:{}]

-P (حرف بزرگ) همه‌ی پورت‌هایی را که image با EXPOSE اعلام کرده، روی پورت‌های تصادفی میزبان publish می‌کند. docker port نشان می‌دهد کدام عدد انتخاب شد. نکته: EXPOSE در Dockerfile فقط مستندات است و پورتی را باز نمی‌کند؛ باز کردن واقعی فقط با -p یا -P است.

Terminal window
docker run -d --name lx-p5 -p 8323:80/tcp nginx:alpine >/dev/null
sleep 2
docker port lx-p5
خروجی
80/tcp -> 0.0.0.0:8323
80/tcp -> [::]:8323

پیش‌فرض TCP است. برای UDP می‌نویسی -p 5353:53/udp (مثلاً برای DNS). می‌توانی چند -p پشت هم بدهی.

مثال ۶: نقشه‌ی کل پورت‌ها

Section titled “مثال ۶: نقشه‌ی کل پورت‌ها”
Terminal window
docker ps --filter name=lx-p --format 'table {{.Names}}\t{{.Ports}}' | sed 's/, \[::\][^ ,]*//g'
خروجی
NAMES PORTS
lx-p5 0.0.0.0:8323->80/tcp
lx-p4 0.0.0.0:55001->80/tcp
lx-p3 127.0.0.1:8322->80/tcp
lx-p2 0.0.0.0:8321->80/tcp
lx-p1 0.0.0.0:8320->80/tcp

ستون Ports در docker ps خلاصه‌ی همین نقشه است: 0.0.0.0:8320->80/tcp یعنی «همه‌ی آدرس‌ها، پورت ۸۳۲۰ ← پورت ۸۰ کانتینر».

پشت پرده: چطور کار می‌کند؟

Section titled “پشت پرده: چطور کار می‌کند؟”

روی لینوکس، daemon هنگام -p قانون‌های NAT در فایروال هسته (iptables یا nftables) می‌گذارد و گاهی یک پروکسی کوچک (docker-proxy) هم برای ترافیک localhost اجرا می‌کند. روی مک و ویندوز، Docker Desktop پورت را روی سیستم اصلی گوش می‌کند و ترافیک را به VM لینوکسی (و کانتینر) پروکسی می‌کند. در هر دو حالت از نگاه تو یک چیز است: localhost:پورت-میزبان ← پورت-کانتینر.

دو نتیجه‌ی مهم:

  • پورت داخل کانتینر مستقل است. دو کانتینر می‌توانند هر دو روی ۸۰ گوش بدهند؛ فقط نگاشت به پورت‌های میزبان باید یکتا باشد.
  • برنامه باید روی 0.0.0.0 داخل کانتینر گوش بدهد. اگر فقط روی 127.0.0.1 داخل کانتینر گوش بدهد، ترافیک وارد‌شده از بیرون کانتینر (که از رابط شبکه‌ی دیگری می‌آید) به آن نمی‌رسد (اشتباه ۳ پایین را ببین).
شکل معنی
-p 8080:80 پورت 8080 همه‌ی آدرس‌های میزبان ← پورت 80 کانتینر
-p 127.0.0.1:8080:80 فقط روی خود میزبان
-p 80 پورت 80 کانتینر ← یک پورت تصادفی میزبان
-p 5353:53/udp پروتکل UDP
-P همه‌ی پورت‌های EXPOSE شده روی پورت‌های تصادفی
docker port N نمایش نقشه‌ی پورت‌های کانتینر
سؤال دستور بررسی
پورت publish شده؟ docker port NAME یا ستون Ports در docker ps
میزبان روی پورت گوش می‌دهد؟ lsof -nP -iTCP:PORT -sTCP:LISTEN (مک) یا ss -tulpn (لینوکس)
سرویس جواب می‌دهد؟ curl -i localhost:PORT
Terminal window
docker run -d --name lx-p6 -p 8320:80 nginx:alpine
خروجی
026cdf359a039bc965fd5e35302efaf268da6ec7084d42a07c86b769c6b5ecdc
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint lx-p6 (f41aed1a528703cbde20a913dda864da99ea2831e8e0722195a6f93a0fc31bf8): Bind for 0.0.0.0:8320 failed: port is already allocated
Run 'docker run --help' for more information

پورت ۸۳۲۰ سیستم قبلاً به یک کانتینر دیگر (lx-p1) داده شده. راه‌حل: پورت دیگری بده، یا با docker ps کانتینر صاحب آن پورت را پیدا و متوقف کن.

۲) وصل شدن به پورت اشتباه

Section titled “۲) وصل شدن به پورت اشتباه”
Terminal window
curl -s -m 3 localhost:8329 || echo "(کد خروج curl: $?)"
خروجی
(کد خروج curl: 7)

روی پورت ۸۳۲۹ هیچ‌چیز گوش نمی‌دهد (Connection refused یعنی کسی آنجا نیست). راه‌حل: با docker port یا docker ps پورت درست میزبان را پیدا کن؛ ترتیب را اشتباه نگیر (میزبان:کانتینر).

۳) برنامه فقط روی localhost داخل کانتینر گوش می‌دهد

Section titled “۳) برنامه فقط روی localhost داخل کانتینر گوش می‌دهد”
Terminal window
docker run -d --name lx-p6 -p 8324:8000 python:3.12-slim python -m http.server 8000 --bind 127.0.0.1 >/dev/null
sleep 3
curl -s -m 5 -o /dev/null -w "HTTP %{http_code}\n" localhost:8324 || echo "(کد خروج curl: $?)"
خروجی
HTTP 000
(کد خروج curl: 56)

پورت درست publish شده، ولی سرور Python فقط روی 127.0.0.1 داخل کانتینر گوش می‌دهد. ترافیک ورودی از بیرون از رابط دیگری می‌رسد و به سرور نمی‌رسد؛ نتیجه Empty reply from server یا اتصال بسته (کد ۵۲ یا ۵۶). راه‌حل: برنامه را با --bind 0.0.0.0 (یا host: 0.0.0.0) اجرا کن، بعد دوباره تست کن:

Terminal window
docker rm -f lx-p6 >/dev/null
docker run -d --name lx-p6 -p 8324:8000 python:3.12-slim python -m http.server 8000 --bind 0.0.0.0 >/dev/null
sleep 3
curl -s -m 5 -o /dev/null -w "HTTP %{http_code}\n" localhost:8324
خروجی
HTTP 200

۴) باز کردن پورت دیتابیس به همه

Section titled “۴) باز کردن پورت دیتابیس به همه”

-p 5432:5432 پایگاه‌داده را به کل شبکه نشان می‌دهد. راه‌حل: -p 127.0.0.1:5432:5432 یا اصلاً publish نکن و فقط از داخل شبکه‌ی داکر وصل شو (درس شبکه).

✎ تمرینآسان

یک nginx روی پورت 8325 بالا بیاور (lx-ex1) و نقشه‌ی پورتش را با docker port نشان بده.

دیدن جواب
Terminal window
docker run -d --name lx-ex1 -p 8325:80 nginx:alpine >/dev/null; sleep 2
docker port lx-ex1
docker rm -f lx-ex1 >/dev/null
خروجی
80/tcp -> 0.0.0.0:8325
80/tcp -> [::]:8325
✎ تمرینمتوسط

دو کانتینر nginx روی دو پورت مختلف (8326 و 8327) اجرا کن و با curl ثابت کن هر دو جواب 200 می‌دهند. این همان تمرین اصلی درس است.

دیدن جواب
Terminal window
docker run -d --name lx-ex2a -p 8326:80 nginx:alpine >/dev/null
docker run -d --name lx-ex2b -p 8327:80 nginx:alpine >/dev/null; sleep 2
for p in 8326 8327; do curl -s -o /dev/null -w "$p -> %{http_code}\n" localhost:$p; done
docker rm -f lx-ex2a lx-ex2b >/dev/null
خروجی
8326 -> 200
8327 -> 200
✎ تمرینسخت

یک nginx فقط روی localhost (127.0.0.1:8328) بالا بیاور. ثابت کن docker port همین را نشان می‌دهد و با curl به 127.0.0.1 جواب می‌گیری. بعد توضیح بده چرا برای یک دیتابیس این شکل بهتر از -p 5432:5432 است.

دیدن جواب
Terminal window
docker run -d --name lx-ex3 -p 127.0.0.1:8328:80 nginx:alpine >/dev/null; sleep 2
docker port lx-ex3
curl -s -o /dev/null -w "HTTP %{http_code}\n" 127.0.0.1:8328
docker rm -f lx-ex3 >/dev/null
خروجی
80/tcp -> 127.0.0.1:8328
HTTP 200

با 127.0.0.1 فقط خود سیستم می‌تواند به پورت وصل شود؛ هیچ دستگاه دیگری در شبکه به دیتابیس دسترسی ندارد، که از نظر امنیتی خیلی بهتر از باز کردن آن به همه‌ی آدرس‌هاست.

؟ آزمونک
  1. در -p 8080:80 عدد 8080 مربوط به کجاست؟

  2. دو کانتینر می‌توانند هر دو داخل خودشان روی پورت 80 گوش بدهند؟

  3. EXPOSE در Dockerfile چه می‌کند؟

  4. پورت publish شده است ولی curl اتصال را قطع می‌کند. یک علت محتمل؟

  5. چرا برای دیتابیس -p 127.0.0.1:5432:5432 امن‌تر است؟

  • بدون -p، سرویس داخل کانتینر از بیرون در دسترس نیست؛ -p میزبان:کانتینر پل می‌سازد.
  • -p 127.0.0.1:... فقط روی خود سیستم باز می‌کند؛ -P پورت‌های EXPOSE شده را تصادفی publish می‌کند؛ /udp برای UDP.
  • EXPOSE فقط مستندات است.
  • «port is already allocated» یعنی پورت میزبان گرفته شده؛ برنامه‌ی داخل کانتینر باید روی 0.0.0.0 گوش بدهد.
  • docker port و ستون Ports نقشه را نشان می‌دهند.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker run -p 8080:80 nginxپورت 8080 میزبان ← 80 کانتینر
docker run -p 127.0.0.1:8080:80 nginxفقط روی localhost
docker run -P nginxپورت‌های EXPOSE روی پورت‌های تصادفی
docker run -p 5353:53/udp IMGپروتکل UDP
docker port NAMEنقشه‌ی پورت‌های کانتینر
docker ps --format "table {{.Names}}\t{{.Ports}}"پورت‌های همه‌ی کانتینرها
curl -i localhost:8080تست سرویس