توی این درس یاد میگیری چرا یک سرویس داخل کانتینر بهصورت پیشفرض از بیرون در دسترس نیست و چطور با publish پورت (-p) آن را باز میکنی. فرق پورت میزبان (host) و پورت کانتینر را میفهمی، با -p 127.0.0.1:8080:80 پورت را فقط روی خود سیستم باز میکنی، با -P پورتهای تصادفی میگیری و با docker port نقشهی پورتها را میخوانی. پرتکرارترین خطای داکر، «port is already allocated»، را هم میشناسی.
تشبیه: شمارهی داخلی و خط شهری
Section titled “تشبیه: شمارهی داخلی و خط شهری”فرض کن یک شرکت (کانتینر) داری که تلفن داخلی ۸۰ آن وبسرور است. تلفن داخلی ۸۰ از بیرون شرکت قابل شمارهگیری نیست. باید در اپراتور (میزبان) تنظیم کنی: «هر تماس با خط شهری ۸۰۸۰ را به داخلی ۸۰ وصل کن.» این همان publish پورت است: -p 8080:80 یعنی «خط شهری ۸۰۸۰ ← داخلی ۸۰».
مسیر یک درخواست
Section titled “مسیر یک درخواست”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: بدون -p و با -p
Section titled “مثال ۱: بدون -p و با -p”docker run -d --name lx-p1 nginx:alpine >/dev/nullsleep 2echo "بدون -p:"; curl -s -o /dev/null -m 3 -w " HTTP %{http_code}\n" localhost:8320 || echo " در دسترس نیست"docker rm -f lx-p1 >/dev/nulldocker run -d --name lx-p1 -p 8320:80 nginx:alpine >/dev/nullsleep 2echo "با -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 “مثال ۲: دو کانتینر، دو پورت”docker run -d --name lx-p2 -p 8321:80 nginx:alpine >/dev/nullsleep 2for 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”docker run -d --name lx-p3 -p 127.0.0.1:8322:80 nginx:alpine >/dev/nullsleep 2docker port lx-p1docker port lx-p380/tcp -> 0.0.0.0:832080/tcp -> [::]:832080/tcp -> 127.0.0.1:8322-p 8320:80 بهطور پیشفرض روی همهی آدرسهای میزبان (0.0.0.0) باز میشود؛ یعنی هر دستگاهی در شبکهی تو میتواند به آن برسد. -p 127.0.0.1:8322:80 فقط روی خود سیستم باز میکند. برای دیتابیسها و ابزارهای داخلی همیشه این شکل را بنویس.
کدام دستور پورت را فقط روی خود سیستم باز میکند؟
آدرس 127.0.0.1 اول آمده، پس فقط loopback میزبان دسترسی دارد.
مثال ۴: پورت تصادفی با -P
Section titled “مثال ۴: پورت تصادفی با -P”docker run -d --name lx-p4 -P nginx:alpine >/dev/nullsleep 2docker port lx-p4docker inspect lx-p4 --format 'EXPOSE شده: {{.Config.ExposedPorts}}'80/tcp -> 0.0.0.0:55001EXPOSE شده: map[80/tcp:{}]-P (حرف بزرگ) همهی پورتهایی را که image با EXPOSE اعلام کرده، روی پورتهای تصادفی میزبان publish میکند. docker port نشان میدهد کدام عدد انتخاب شد. نکته: EXPOSE در Dockerfile فقط مستندات است و پورتی را باز نمیکند؛ باز کردن واقعی فقط با -p یا -P است.
مثال ۵: نوع پروتکل
Section titled “مثال ۵: نوع پروتکل”docker run -d --name lx-p5 -p 8323:80/tcp nginx:alpine >/dev/nullsleep 2docker port lx-p580/tcp -> 0.0.0.0:832380/tcp -> [::]:8323پیشفرض TCP است. برای UDP مینویسی -p 5353:53/udp (مثلاً برای DNS). میتوانی چند -p پشت هم بدهی.
مثال ۶: نقشهی کل پورتها
Section titled “مثال ۶: نقشهی کل پورتها”docker ps --filter name=lx-p --format 'table {{.Names}}\t{{.Ports}}' | sed 's/, \[::\][^ ,]*//g'NAMES PORTSlx-p5 0.0.0.0:8323->80/tcplx-p4 0.0.0.0:55001->80/tcplx-p3 127.0.0.1:8322->80/tcplx-p2 0.0.0.0:8321->80/tcplx-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داخل کانتینر گوش بدهد، ترافیک واردشده از بیرون کانتینر (که از رابط شبکهی دیگری میآید) به آن نمیرسد (اشتباه ۳ پایین را ببین).
جدولهای مرجع
Section titled “جدولهای مرجع”| شکل | معنی |
|---|---|
-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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) «port is already allocated»
Section titled “۱) «port is already allocated»”docker run -d --name lx-p6 -p 8320:80 nginx:alpine026cdf359a039bc965fd5e35302efaf268da6ec7084d42a07c86b769c6b5ecdcdocker: 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 “۲) وصل شدن به پورت اشتباه”curl -s -m 3 localhost:8329 || echo "(کد خروج curl: $?)"(کد خروج curl: 7)روی پورت ۸۳۲۹ هیچچیز گوش نمیدهد (Connection refused یعنی کسی آنجا نیست). راهحل: با docker port یا docker ps پورت درست میزبان را پیدا کن؛ ترتیب را اشتباه نگیر (میزبان:کانتینر).
۳) برنامه فقط روی localhost داخل کانتینر گوش میدهد
Section titled “۳) برنامه فقط روی localhost داخل کانتینر گوش میدهد”docker run -d --name lx-p6 -p 8324:8000 python:3.12-slim python -m http.server 8000 --bind 127.0.0.1 >/dev/nullsleep 3curl -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) اجرا کن، بعد دوباره تست کن:
docker rm -f lx-p6 >/dev/nulldocker run -d --name lx-p6 -p 8324:8000 python:3.12-slim python -m http.server 8000 --bind 0.0.0.0 >/dev/nullsleep 3curl -s -m 5 -o /dev/null -w "HTTP %{http_code}\n" localhost:8324HTTP 200۴) باز کردن پورت دیتابیس به همه
Section titled “۴) باز کردن پورت دیتابیس به همه”-p 5432:5432 پایگاهداده را به کل شبکه نشان میدهد. راهحل: -p 127.0.0.1:5432:5432 یا اصلاً publish نکن و فقط از داخل شبکهی داکر وصل شو (درس شبکه).
یک nginx روی پورت 8325 بالا بیاور (lx-ex1) و نقشهی پورتش را با docker port نشان بده.
دیدن جواب
docker run -d --name lx-ex1 -p 8325:80 nginx:alpine >/dev/null; sleep 2docker port lx-ex1docker rm -f lx-ex1 >/dev/null80/tcp -> 0.0.0.0:832580/tcp -> [::]:8325دو کانتینر nginx روی دو پورت مختلف (8326 و 8327) اجرا کن و با curl ثابت کن هر دو جواب 200 میدهند. این همان تمرین اصلی درس است.
دیدن جواب
docker run -d --name lx-ex2a -p 8326:80 nginx:alpine >/dev/nulldocker run -d --name lx-ex2b -p 8327:80 nginx:alpine >/dev/null; sleep 2for p in 8326 8327; do curl -s -o /dev/null -w "$p -> %{http_code}\n" localhost:$p; donedocker rm -f lx-ex2a lx-ex2b >/dev/null8326 -> 2008327 -> 200یک nginx فقط روی localhost (127.0.0.1:8328) بالا بیاور. ثابت کن docker port همین را نشان میدهد و با curl به 127.0.0.1 جواب میگیری. بعد توضیح بده چرا برای یک دیتابیس این شکل بهتر از -p 5432:5432 است.
دیدن جواب
docker run -d --name lx-ex3 -p 127.0.0.1:8328:80 nginx:alpine >/dev/null; sleep 2docker port lx-ex3curl -s -o /dev/null -w "HTTP %{http_code}\n" 127.0.0.1:8328docker rm -f lx-ex3 >/dev/null80/tcp -> 127.0.0.1:8328HTTP 200با 127.0.0.1 فقط خود سیستم میتواند به پورت وصل شود؛ هیچ دستگاه دیگری در شبکه به دیتابیس دسترسی ندارد، که از نظر امنیتی خیلی بهتر از باز کردن آن به همهی آدرسهاست.
آزمونک
Section titled “آزمونک”در -p 8080:80 عدد 8080 مربوط به کجاست؟
ترتیب: پورت-میزبان:پورت-کانتینر.
دو کانتینر میتوانند هر دو داخل خودشان روی پورت 80 گوش بدهند؟
هر کانتینر شبکهی خودش را دارد.
EXPOSE در Dockerfile چه میکند؟
باز کردن واقعی فقط با -p یا -P است.
پورت publish شده است ولی curl اتصال را قطع میکند. یک علت محتمل؟
باید روی 0.0.0.0 داخل کانتینر گوش بدهد.
چرا برای دیتابیس -p 127.0.0.1:5432:5432 امنتر است؟
بدون آدرس، پورت روی همهی آدرسهای میزبان باز میشود.
جمعبندی
Section titled “جمعبندی”- بدون
-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 | تست سرویس |