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

DNS بین کانتینرها

توی این درس یاد می‌گیری چرا در تنظیمات برنامه‌ات به‌جای آدرس IP می‌نویسی db یا redis و همه‌چیز کار می‌کند: روی شبکه‌های سفارشی، داکر یک DNS داخلی (آدرس 127.0.0.11 داخل هر کانتینر) دارد که اسم کانتینرها و سرویس‌ها را به IP تبدیل می‌کند. می‌فهمی چرا شبکه‌ی پیش‌فرض این را ندارد، با alias چند اسم می‌دهی، دو کانتینر با یک alias را می‌بینی که DNS هر دو IP را برمی‌گرداند (round-robin)، و یک اپ را با اسم کانتینر به دیتابیس وصل می‌کنی. دستور آزمایش: docker exec app ping db.

تشبیه: دفترچه‌ی تلفن شرکت

Section titled “تشبیه: دفترچه‌ی تلفن شرکت”

در یک شرکت، به‌جای حفظ کردن شماره‌ی داخلی همه، می‌گویی «به مدیر فروش وصل کن». دفترچه‌ی تلفن (DNS) اسم را به شماره تبدیل می‌کند. اگر کسی نقل‌مکان کند (IP عوض شود)، فقط دفترچه به‌روز می‌شود و تو همچنان «مدیر فروش» را صدا می‌زنی. در داکر، IP کانتینرها با هر ساخت دوباره تغییر می‌کند؛ پس هیچ‌وقت IP را در تنظیمات ننویس، اسم بنویس.

برنامه اسم db را می‌پرسد؛ درخواست به DNS داخلی داکر (127.0.0.11) می‌رود که از جدول کانتینرهای همان شبکه جواب می‌دهد.

مثال ۱: DNS روی شبکه‌ی پیش‌فرض و سفارشی

Section titled “مثال ۱: DNS روی شبکه‌ی پیش‌فرض و سفارشی”
Terminal window
docker run -d --name lx-dns1 alpine sleep 300 >/dev/null
echo "bridge پیش‌فرض، resolv.conf:"; docker exec lx-dns1 cat /etc/resolv.conf | grep nameserver | head -1
docker network create lx-dnsnet >/dev/null
docker run -d --name lx-dns2 --network lx-dnsnet alpine sleep 300 >/dev/null
echo "شبکه‌ی سفارشی، resolv.conf:"; docker exec lx-dns2 cat /etc/resolv.conf | grep -E "nameserver|options" | head -2
خروجی
bridge پیش‌فرض، resolv.conf:
nameserver 192.168.65.7
شبکه‌ی سفارشی، resolv.conf:
nameserver 127.0.0.11
options ndots:0

روی شبکه‌ی سفارشی nameserver برابر 127.0.0.11 است؛ همان DNS داخلی داکر. روی شبکه‌ی پیش‌فرض، resolv.conf از میزبان کپی می‌شود و DNS داخلی داکر وجود ندارد. این تفاوت علت اینکه فقط شبکه‌ی سفارشی resolve اسم دارد.

مثال ۲: resolve اسم کانتینر

Section titled “مثال ۲: resolve اسم کانتینر”
Terminal window
docker run -d --name lx-dns3 --network lx-dnsnet alpine sleep 300 >/dev/null
docker exec lx-dns2 nslookup lx-dns3 2>&1 | grep -E "^(Name|Address):" | tail -2
docker exec lx-dns2 ping -c 1 -W 2 lx-dns3 | sed -n '1p;3p'
echo "--- روی bridge پیش‌فرض:"
docker exec lx-dns1 ping -c 1 -W 2 lx-dns3 2>&1 | head -1
خروجی
Name: lx-dns3
Address: 172.20.0.3
PING lx-dns3 (172.20.0.3): 56 data bytes
--- روی bridge پیش‌فرض:
ping: bad address 'lx-dns3'

داخل lx-dns2، اسم lx-dns3 به IP تبدیل شد و ping کار کرد. کانتینر lx-dns1 (روی شبکه‌ی پیش‌فرض) نمی‌تواند lx-dns3 را resolve کند، حتی اگر روی یک میزبان باشند.

⚡ بررسی سریع

آدرس DNS داخلی داکر داخل کانتینر روی شبکه‌ی سفارشی چیست؟

مثال ۳: alias: چند اسم برای یک کانتینر

Section titled “مثال ۳: alias: چند اسم برای یک کانتینر”
Terminal window
docker run -d --name lx-dnsdb --network lx-dnsnet --network-alias database --network-alias db alpine sleep 300 >/dev/null
for n in lx-dnsdb database db; do echo "$n -> $(docker exec lx-dns2 getent hosts $n | awk '{print $1}')"; done
خروجی
lx-dnsdb -> 172.20.0.4
database -> 172.20.0.4
db -> 172.20.0.4

با --network-alias یک کانتینر چند اسم دیگر در همان شبکه می‌گیرد. هر سه اسم به یک IP اشاره می‌کنند. برای اینکه اپ فقط اسم db را بداند و مستقل از اسم واقعی کانتینر باشد، alias عالی است (در Compose اسم هر سرویس خودکار یک alias است).

مثال ۴: round-robin: یک alias، چند کانتینر

Section titled “مثال ۴: round-robin: یک alias، چند کانتینر”
Terminal window
docker run -d --name lx-w1 --network lx-dnsnet --network-alias web nginx:alpine >/dev/null
docker run -d --name lx-w2 --network lx-dnsnet --network-alias web nginx:alpine >/dev/null
sleep 2
docker exec lx-dns2 nslookup web 2>&1 | grep "^Address:" | tail -2
خروجی
Address: 172.20.0.6
Address: 172.20.0.5

وقتی دو کانتینر یک alias دارند، DNS هر دو IP را برمی‌گرداند و ترتیب را می‌چرخاند. یک load-balancing ساده در سطح DNS (یک راهکار کامل با Nginx یا Traefik در دوره‌ی Nginx).

مثال ۵: اپ به دیتابیس با اسم

Section titled “مثال ۵: اپ به دیتابیس با اسم”

یک «دیتابیس» (nginx که نقش یک سرویس را دارد) و یک «اپ» که با اسم به آن وصل می‌شود:

Terminal window
docker rm -f lx-w1 lx-w2 >/dev/null
docker run -d --name lx-dnsapp --network lx-dnsnet alpine sleep 300 >/dev/null
docker run -d --name lx-dns4 --network lx-dnsnet --network-alias backend nginx:alpine >/dev/null
sleep 2
docker exec lx-dnsapp wget -qO- http://backend | head -3
خروجی
<!DOCTYPE html>
<html>
<head>

اپ هیچ IP ای نمی‌دانست؛ فقط آدرس http://backend را زد و DNS داخلی آن را به کانتینر درست رساند. حالا ثابت کنیم IP عوض شود، اسم همچنان کار می‌کند:

Terminal window
echo "IP قبل: $(docker inspect lx-dns4 --format '{{(index .NetworkSettings.Networks "lx-dnsnet").IPAddress}}')"
docker rm -f lx-dns4 >/dev/null
docker run -d --name lx-dns8 --network lx-dnsnet alpine sleep 120 >/dev/null # IP آزاد‌شده را یک کانتینر دیگر می‌گیرد
docker run -d --name lx-dns4 --network lx-dnsnet --network-alias backend nginx:alpine >/dev/null; sleep 2
echo "IP بعد: $(docker inspect lx-dns4 --format '{{(index .NetworkSettings.Networks "lx-dnsnet").IPAddress}}')"
docker exec lx-dnsapp wget -qO- http://backend | head -1
خروجی
IP قبل: 172.20.0.6
IP بعد: 172.20.0.7
<!DOCTYPE html>

حتی با IP جدید، http://backend بدون تغییر کار کرد؛ برای همین هرگز IP را در تنظیمات ننویس.

مثال ۶: DNS بیرونی هم کار می‌کند

Section titled “مثال ۶: DNS بیرونی هم کار می‌کند”
Terminal window
docker exec lx-dnsapp nslookup example.com 2>&1 | grep -E "^Name:" | head -1
خروجی
Name: example.com

برای اسم‌هایی که بیرون از شبکه‌ی داکرند (مثل example.com) DNS داخلی درخواست را به DNS میزبان forward می‌کند. پس هر دو نوع اسم کار می‌کنند.

وقتی کانتینری روی یک شبکه‌ی سفارشی شروع می‌شود، dockerd یک resolver داخلی کوچک را وارد network namespace آن می‌کند و /etc/resolv.conf را به 127.0.0.11 اشاره می‌دهد. این resolver جدول اسم کانتینر/alias → IP را برای همان شبکه نگه می‌دارد؛ اگر اسم در جدول نبود، درخواست را به DNS های میزبان می‌فرستد. شبکه‌ی bridge پیش‌فرض قدیمی‌تر است و این resolver را ندارد (روش قدیمی --link برای آن بود که منسوخ شده).

دو نکته‌ی عملی:

  • DNS داخلی محدود به همان شبکه است: کانتینری که روی شبکه‌ی دیگر است، اسم را نمی‌بیند (مگر به هر دو شبکه وصل باشد).
  • اسم‌ها حساس به حروف بزرگ و کوچک نیستند (استاندارد DNS)، ولی بهتر است مثل kebab-case یکدست بنویسی.
ابزار کاربرد
docker exec app ping db آزمون ارتباط با اسم
docker exec app nslookup db دیدن IP که DNS می‌دهد
docker exec app getent hosts db resolve مثل خود برنامه‌ها
docker exec app cat /etc/resolv.conf دیدن nameserver
docker run --network-alias A alias اضافه
docker network inspect N کانتینرها و IP ها
شبکه DNS اسم کانتینر؟
bridge پیش‌فرض خیر
شبکه‌ی سفارشی (docker network create) بله (127.0.0.11)
host DNS میزبان (بدون resolve کانتینرها)
Compose بله (اسم سرویس‌ها)

۱) استفاده از IP به‌جای اسم

Section titled “۱) استفاده از IP به‌جای اسم”

مثال ۵: IP با ساخت دوباره عوض می‌شود. راه‌حل: اسم کانتینر/سرویس/alias.

۲) انتظار DNS روی bridge پیش‌فرض

Section titled “۲) انتظار DNS روی bridge پیش‌فرض”
Terminal window
docker exec lx-dns1 nslookup lx-dns3 2>&1 | grep -E "can't find|NXDOMAIN" | head -1
خروجی
** server can't find lx-dns3: NXDOMAIN

روی شبکه‌ی پیش‌فرض اسم resolve نمی‌شود. راه‌حل: شبکه‌ی سفارشی یا Compose.

۳) کانتینرها در شبکه‌های مختلف

Section titled “۳) کانتینرها در شبکه‌های مختلف”
Terminal window
docker network create lx-dnsnet2 >/dev/null
docker run -d --name lx-dns5 --network lx-dnsnet2 alpine sleep 120 >/dev/null
docker exec lx-dns2 ping -c 1 -W 2 lx-dns5 2>&1 | head -1
docker rm -f lx-dns5 >/dev/null; docker network rm lx-dnsnet2 >/dev/null
خروجی
ping: bad address 'lx-dns5'

اسم در شبکه‌ی دیگر دیده نمی‌شود. راه‌حل: هر دو را به یک شبکه وصل کن (docker network connect).

اسم با زیرخط (_) یا کاراکتر عجیب در بعضی کتابخانه‌ها مشکل‌ساز است (hostname معتبر نیست). راه‌حل: از حروف، عدد و خط تیره استفاده کن.

بعضی برنامه‌ها (مثل JVM یا کتابخانه‌های قدیمی) IP را یک بار resolve و برای همیشه cache می‌کنند. اگر کانتینر مقصد دوباره ساخته شود، اتصال قدیمی خراب است. راه‌حل: TTL و retry در برنامه یا restart اپ.

✎ تمرینآسان

آدرس nameserver داخل یک کانتینر روی شبکه‌ی سفارشی را بخوان.

دیدن جواب
Terminal window
docker exec lx-dns2 cat /etc/resolv.conf | grep nameserver
خروجی
nameserver 127.0.0.11
✎ تمرینمتوسط

تمرین اصلی: یک اپ را با اسم کانتینر به دیتابیس وصل کن. روی شبکه‌ی lx-dnsnet یک nginx با alias database و یک کانتینر client بساز و از client با wget به http://database درخواست بزن.

دیدن جواب
Terminal window
docker run -d --name lx-dns6 --network lx-dnsnet --network-alias database nginx:alpine >/dev/null
docker run -d --name lx-dns7 --network lx-dnsnet alpine sleep 120 >/dev/null; sleep 2
docker exec lx-dns7 wget -qO- http://database | grep -o "<title>.*</title>"
docker rm -f lx-dns6 lx-dns7 >/dev/null
خروجی
<title>Welcome to nginx!</title>
✎ تمرینسخت

ثابت کن با دو کانتینر هم‌alias، nslookup هر دو IP را برمی‌گرداند. بعد یکی را حذف کن و ثابت کن حالا فقط یک IP مانده است.

دیدن جواب
Terminal window
docker run -d --name lx-r1 --network lx-dnsnet --network-alias svc nginx:alpine >/dev/null
docker run -d --name lx-r2 --network lx-dnsnet --network-alias svc nginx:alpine >/dev/null; sleep 2
echo "دو کانتینر: $(docker exec lx-dns2 nslookup svc 2>&1 | grep -c '^Address:.*172') آدرس"
docker rm -f lx-r2 >/dev/null; sleep 1
echo "یک کانتینر: $(docker exec lx-dns2 nslookup svc 2>&1 | grep -c '^Address:.*172') آدرس"
docker rm -f lx-r1 >/dev/null
خروجی
دو کانتینر: 2 آدرس
یک کانتینر: 1 آدرس
؟ آزمونک
  1. DNS داخلی داکر فقط روی کدام شبکه‌ها اسم کانتینر را resolve می‌کند؟

  2. چرا نباید IP کانتینر را در تنظیمات برنامه بنویسی؟

  3. دو کانتینر با یک network-alias یکسان…

  4. کدام دستور resolve اسم را آزمایش می‌کند؟

  5. اگر اسم را کانتینری در شبکه‌ی دیگر داشته باشد…

  • روی شبکه‌ی سفارشی هر کانتینر 127.0.0.11 را به‌عنوان DNS دارد که اسم کانتینرها و alias ها را به IP تبدیل می‌کند.
  • bridge پیش‌فرض این resolver را ندارد؛ پس برای اسم، شبکه‌ی سفارشی (یا Compose) بساز.
  • --network-alias اسم اضافه می‌دهد؛ چند کانتینر با یک alias = round-robin DNS.
  • IP را در تنظیمات ننویس؛ از اسم استفاده کن (پایدار در برابر ساخت دوباره).
  • اسم‌های بیرونی به DNS میزبان forward می‌شوند.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker exec app ping dbتست ارتباط با اسم
docker exec app nslookup dbIP که DNS می‌دهد
docker exec app getent hosts dbresolve مثل برنامه‌ها
docker exec app cat /etc/resolv.confnameserver (127.0.0.11)
docker run --network N --network-alias A IMGalias در شبکه
docker network inspect Nکانتینرها و IP ها