شبکهسازی Docker Compose؛ DNS، host mode و پورتها
شبکه پیشفرض bridge، DNS با نام سرویس، کاربرد واقعی host mode، اشتراک شبکه میان پروژهها و پورتی که با انتشار مستقیم از UFW عبور میکند را بشناسید.
Compose پیش از راهاندازی برنامه چه چیزی ایجاد میکند
شبکهسازی Docker Compose با یک قاعده آغاز میشود: docker compose up یک شبکه خصوصی برای پروژه ایجاد میکند، همه سرویسها را به آن متصل میکند و به این سرویسها اجازه میدهد از طریق نام سرویس به یکدیگر دسترسی داشته باشند. برای این کار لازم نیست حتی یک خط networks: بنویسید. بیشتر سردرگمیها درباره شبکهسازی Compose از اینجا ناشی میشود که کاربران نمیدانند این مقدار پیشفرض از قبل وجود دارد.
در اینجا یک فایل کوچک وجود دارد. آن را با نام compose.yaml در پوشهای با نام shop ذخیره کنید.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleآن را راهاندازی کنید و مواردی را که Docker ایجاد کرده است بررسی کنید:
docker compose up -d
docker network lsاکنون فهرست شامل شبکهای با نام shop_default است. Compose نام <project>_default را برای آن تعیین میکند و نام پروژه بهطور پیشفرض همان نام پوشه با حروف کوچک است. میتوانید این نام را با docker compose -p myproject up -d یا با name: myproject در سطح بالای فایل تغییر دهید. درایور شبکه bridge است که یک سوئیچ مجازی درون میزبان محسوب میشود. هر کانتینر یک نشانی روی یک subnet خصوصی دریافت میکند و ترافیک خروجی هنگام خروج، به نشانی میزبان ترجمه میشود.
docker compose down این شبکه را دوباره حذف میکند. به همین دلیل، یک کانتینر قدیمی از پروژهای قبلی میتواند مانع حذف شبکه شود: Docker با error while removing network: network shop_default has active endpoints خطا میدهد و راهحل، متوقف کردن یا حذف کانتینری است که هنوز به شبکه متصل است.
اگر Compose برای شما جدید است، مطالعه ساختار فایل Compose و فرمانهای چرخه حیات در ابتدا مفید است، زیرا تمام مطالب بعدی فرض میکنند که میتوانید یک پروژه را راهاندازی و متوقف کنید.
DNS بر اساس نام سرویس، بخشی است که مبتدیان از آن غافل میشوند
در هر شبکهای که کاربر تعریف کرده باشد، Docker یک سرور DNS داخلی اجرا میکند که هر کانتینر آن را در 127.0.0.11 مشاهده میکند. این سرور نام سرویسها را به نشانیهای فعلی کانتینرها resolve میکند. بنابراین web بدون هیچ پیکربندی، از طریق نام میزبان db و روی پورت 5432 به پایگاه داده متصل میشود.
docker compose exec web getent hosts dbاین دستور خطی مانند 172.18.0.2 db چاپ میکند. اگر چیزی چاپ نشود، دو سرویس در یک شبکه نیستند.
اشتباهی که تقریباً همه یکبار مرتکب میشوند، استفاده از localhost در پیکربندی برنامه است. داخل یک کانتینر، localhost به همان کانتینر اشاره میکند، نه به میزبان و نه به سرویس دیگر. کلاینتهای Postgres این موضوع را بهروشنی گزارش میکنند:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?رشته اتصال باید postgresql://postgres:example@db:5432/postgres باشد. بخش میزبان، نام سرویس است.
دو نکته وجود دارد که بعداً در زمان صرفهجویی میکنند. نامها به چیزی resolve میشوند که اکنون در حال اجراست؛ بنابراین docker compose up -d --scale web=3 یک نام را با سه نشانی برمیگرداند و کلاینتی که DNS را برای همیشه cache کند، به یک کانتینر ازکارافتاده متصل میماند. همچنین شبکه قدیمی bridge که یک docker run ساده بدون --network از آن استفاده میکند، اصلاً name resolution ندارد؛ به همین دلیل توصیههای مربوط به container links در سال 2016 با چیزی که مشاهده میکنید مطابقت ندارند.
برای اتصال دو سرویس به ports: نیاز ندارید
ports: یک پورت کانتینر را روی میزبان منتشر میکند. این گزینه برای ترافیک ورودی از خارج از Docker است. به ترافیک بین سرویسها ارتباطی ندارد؛ این ترافیک از ابتدا در سراسر محدوده پورت شبکه پروژه کار میکند.
بنابراین ports: - "5432:5432" که بسیاری از افراد به سرویس پایگاه داده خود اضافه میکنند، هیچ فایدهای ندارد و آسیب واقعی ایجاد میکند: این گزینه Postgres را روی رابط عمومی سرور در معرض دسترسی قرار میدهد. آن را حذف کنید. اگر میخواهید برای انجام migration از لپتاپ خود به آن دسترسی داشته باشید، آن را با "127.0.0.1:5432:5432" به loopback متصل کنید و از طریق یک تونل SSH به آن دسترسی پیدا کنید. تفاوت بین listening socket، پورت منتشرشده و قانون firewall در نحوه کار پورتها و سرویسهای در حال گوشدادن در Linux توضیح داده شده است.
expose: در Compose فقط جنبه مستندسازی دارد. این گزینه چیزی را باز نمیکند، زیرا بین کانتینرهای موجود در همان شبکه چیزی بسته نشده است.
چه زمانی network_mode host مناسب است و چه هزینهای دارد
حالت host فضای نام شبکه اختصاصی کانتینر را حذف میکند و به فرایند اجازه میدهد مستقیماً از رابطهای شبکه میزبان استفاده کند.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityبرای استفاده از این حالت دلایل واقعی وجود دارد. فرایندی که باید ترافیک broadcast یا multicast را در شبکه محلی ببیند، مانند فرایند کشف دستگاه برای یک media server یا یک home automation hub، نمیتواند این ترافیک را پشت یک bridge مشاهده کند؛ زیرا bridge این ترافیک را به کانتینر ارسال نمیکند. یک monitoring agent که شمارندههای رابطهای میزبان را میخواند، به رابطهای شبکه میزبان نیاز دارد. همچنین یک مرحله ترجمه آدرس حذف میشود که در نرخهای بالای packet اهمیت دارد.
هزینهها مشخص هستند.
ports: دیگر کار نمیکند. Docker هشدار میدهد که هنگام استفاده از host network mode، پورتهای منتشرشده نادیده گرفته میشوند و کانتینر به هر پورتی که فرایندش باز کند متصل میشود. اگر دو کانتینر در حالت host بخواهند از پورت 8080 استفاده کنند، با یکدیگر تداخل پیدا میکنند و کانتینر دوم با bind: address already in use متوقف میشود.
حل نام بر اساس نام سرویس در هر دو جهت از بین میرود. کانتینر عضو شبکه پروژه نیست، بنابراین نمیتواند db را resolve کند و سرویسهای دیگر نیز نمیتوانند آن را resolve کنند. کانتینر فقط از طریق پورتهای منتشرشده روی میزبان، معمولاً در 127.0.0.1، به آن سرویسها دسترسی دارد.
ایزولهسازی از بین میرود. فرایندی که درون یک کانتینر در حالت host به 0.0.0.0 متصل شود، روی همه رابطهای سرور شما، از جمله رابط عمومی، در حال listening خواهد بود؛ دقیقاً مانند بستهای که با apt نصب شده است. یک مزیت وجود دارد: این ترافیک از مسیر عادی ورودی عبور میکند، بنابراین قوانین UFW روی آن اعمال میشوند؛ این موضوع درباره پورتهای منتشرشده صدق نمیکند.
حالت host یکی از قابلیتهای Linux Docker Engine است. Docker Desktop فقط از نسخه 4.34 به بعد و پس از فعالسازی آن از این حالت پشتیبانی میکند. محدودیتهای بیشتری نیز وجود دارد: کانتینرها نمیتوانند به آدرسهای IP میزبان متصل شوند و فقط TCP و UDP مدیریت میشوند. اگر نیمی از تیم شما روی سرورهای Linux و نیم دیگر روی Docker Desktop کار میکنند، انتظار داشته باشید یک فایل واحد رفتار متفاوتی داشته باشد.
زمانی از حالت host استفاده کنید که به رابطهای شبکه میزبان نیاز دارید. برای رفع مشکل اتصال از آن استفاده نکنید، زیرا معمولاً یک مشکل را با مشکلی پیچیدهتر جایگزین میکند.
دو پروژه Compose را با یک شبکه خارجی متصل کنید
شبکهای که یک پروژه ایجاد میکند، برای پروژه دیگری قابل مشاهده نیست. به همین دلیل، یک reverse proxy در proxy/compose.yaml نمیتواند یک app را در app/compose.yaml ببیند؛ حتی اگر هر دو روی یک server باشند. راهحل، استفاده از شبکهای است که مالکیت آن با هیچیک از پروژهها نباشد.
شبکه را یکبار بهصورت دستی ایجاد کنید:
docker network create edgeسپس آن را در هر پروژه بهعنوان شبکه خارجی تعریف کنید. بخش مربوط به proxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueبخش مربوط به application:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true به Compose میگوید به شبکهای موجود متصل شود و شبکه جدیدی ایجاد نکند، و آن را هنگام docker compose down دستنخورده باقی بگذارد. کلید جداگانه name: اهمیت بیشتری از چیزی دارد که در نگاه اول به نظر میرسد: بدون آن، Compose به دنبال شبکهای با نام دقیق edge میگردد. با استفاده از آن، میتوانید شبکه را در فایل خود با یک نام و روی host با نامی دیگر تعریف کنید.
اگر شبکه وجود نداشته باشد، Compose از شروع کار خودداری میکند و گزارش میدهد که شبکه بهعنوان خارجی تعریف شده، اما پیدا نشد. ابتدا آن را ایجاد کنید.
به نحوه استفاده فایل application از internal توجه کنید. database فقط روی همان شبکه محلی پروژه قرار دارد؛ بنابراین proxy نمیتواند به آن دسترسی داشته باشد و فقط app میتواند به آن متصل شود. افزودن internal: true زیر یک network، محدودیت بیشتری ایجاد میکند و مسیر دسترسی آن به دنیای خارج را کاملاً حذف میکند. این تنظیم برای database یک پیشفرض مناسب است، اما پیش از اعمال آن باید یک پیامد را بدانید: یک container در شبکه داخلی نمیتواند چیزی را download کند. بنابراین، اگر entrypoint هنگام راهاندازی apt-get update یا pip install را اجرا کند، متوقف میشود و سپس با timeout شکست میخورد.
برای یک نمونه کامل و عملی شامل routing rules و certificates، به اجرای چند app پشت یک نمونه Traefik مراجعه کنید.
پورتهای منتشرشده از UFW عبور میکنند
این بخش از شبکهسازی Compose میتواند به یک رخداد امنیتی تبدیل شود. شما پورتی را منتشر میکنید، بررسی میکنید که UFW فعال است و همهچیز بهجز SSH را رد میکند، اما سرویس همچنان از اینترنت قابل دسترسی است.
sudo ufw status
curl http://203.0.113.10:8080UFW میگوید پورت مسدود است. curl همچنان صفحه را برمیگرداند. هیچچیز خراب نیست. Docker قوانین ترجمه نشانی و ارسال را مستقیماً در iptables ایجاد میکند. ترافیک ورودی به پورت منتشرشده کانتینر به کانتینر ارسال میشود، نه به میزبان. بنابراین این ترافیک از زنجیرهای که UFW برای ترافیک مقصد محلی مدیریت میکند عبور نمیکند. قوانین Docker نیز پیش از قوانین UFW تطبیق داده میشوند.
راهحل کوتاه این است که پورت را فقط روی نشانی موردنیاز منتشر کنید:
ports:
- "127.0.0.1:8080:80"با این کار سمت میزبان به loopback متصل میشود. در نتیجه پورت از خود سرور و از طریق تونل SSH قابل دسترسی است، اما از هیچ محل دیگری قابل دسترسی نخواهد بود. نقطه ورود عمومی را پشت یک reverse proxy قرار دهید که پورتهای 80 و 443 را عمداً منتشر میکند. توضیح کامل، ازجمله زنجیره DOCKER-USER برای مواردی که باید پورت منتشرشده را فیلتر کنید، در دلیل عبور مستقیم Docker از UFW و روش رفع آن آمده است.
نحوهٔ اشکالزدایی با چهار فرمان
ابتدا بررسی کنید هر کانتینر واقعاً در کدام شبکه قرار دارد:
docker network inspect shop_defaultبلوک Containers همهٔ کانتینرهای متصل و نشانی آنها را فهرست میکند. سرویسی که در این فهرست وجود ندارد، در شبکهای دیگر قرار دارد، در حالت host اجرا میشود یا در حال اجرا نیست.
تفکیک نام را از یک کانتینر موقتی که به همان شبکه متصل است آزمایش کنید تا به ابزارهای داخل imageهای خودتان نیاز نداشته باشید:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432شکست nslookup به مشکل در تفکیک نام یا عضویت در شبکه اشاره میکند. موفقیت nslookup در حالی که nc شکست میخورد، یعنی سرویس در حال اجراست اما روی آن پورت به درخواستها گوش نمیدهد، یا بهجای 0.0.0.0 روی 127.0.0.1 درون کانتینر خودش گوش میدهد. این وضعیت در سرورهای توسعه رایج است و اصلاح آن باید در نشانی bind برنامه انجام شود، نه در Docker.
یک خطای دیگر نیز وجود دارد که شبیه اشکال Docker به نظر میرسد. اگر کانتینرها بتوانند با یکدیگر ارتباط برقرار کنند اما نتوانند به دستگاهی در شبکهٔ اداری یا VPN شما دسترسی داشته باشند، احتمالاً subnet مربوط به Docker با آن شبکه همپوشانی دارد. Docker بهطور پیشفرض تخصیص نشانی را از 172.17.0.0/16 به بعد انجام میدهد. محدودهٔ pool را در /etc/docker/daemon.json تغییر دهید:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}سپس sudo systemctl restart docker را اجرا کنید و شبکههای تحتتأثیر را دوباره ایجاد کنید، زیرا یک شبکهٔ موجود subnetی را که هنگام ایجاد برای آن تعیین شده است حفظ میکند.
FAQ
چرا کانتینرهای من نمیتوانند با نام سرویس به یکدیگر دسترسی پیدا کنند؟
آنها در یک شبکه نیستند. Compose همه سرویسها را بهصورت خودکار در <project>_default قرار میدهد، اما بهمحض اینکه فهرست networks: را به یک سرویس اضافه کنید، آن فهرست به مجموعه کامل شبکههای آن سرویس تبدیل میشود و شبکه پیشفرض دیگر بهصورت ضمنی در نظر گرفته نمیشود. دستور docker network inspect <network> را اجرا کنید و بررسی کنید که هر دو کانتینر در بلوک Containers ظاهر شوند. همچنین بررسی کنید که هیچیک از سرویسها از network_mode: host استفاده نکنند، زیرا کانتینری که در حالت میزبان اجرا میشود در هیچ شبکه Docker قرار ندارد و نمیتواند نام سرویسها را resolve کند.
آیا برای دسترسی یک سرویس به سرویس دیگر باید پورتها را publish کنم؟
خیر. در یک شبکه Compose، همه پورتهای هر کانتینر برای کانتینرهای دیگر در همان شبکه قابل دسترسی هستند. ports: فقط برای در معرض ترافیک خارج از Docker قرار دادن یک کانتینر وجود دارد و expose: صرفاً جنبه مستندسازی دارد. انتشار پورت پایگاه داده عادتی رایج و پرهزینه است، زیرا پایگاه داده را روی رابط عمومی سرور شما قرار میدهد.
تفاوت شبکهسازی bridge و host چیست؟
bridge برای کانتینر فضای نام شبکه و آدرس مستقل روی یک سوئیچ مجازی ایجاد میکند، نام سرویسها را بهصورت خودکار بین کانتینرها resolve میکند و ترافیک خروجی را ترجمه میکند. host مستقیماً پشته شبکه میزبان را در اختیار کانتینر قرار میدهد: بدون آدرس مستقل، بدون resolve شدن نام سرویس، بدون انتشار پورت و بدون جداسازی از listenerهای دیگر میزبان. bridge حالت پیشفرض است و انتخاب مناسب محسوب میشود، مگر اینکه فرایند به رابطهای میزبان نیاز داشته باشد.
چگونه کانتینرها را از دو فایل Compose متفاوت به یکدیگر متصل کنم؟
با docker network create edge یک شبکه مشترک ایجاد کنید، سپس آن را با external: true در هر دو فایل تعریف کنید و سرویسهایی را که باید با یکدیگر ارتباط داشته باشند به آن متصل کنید. Compose آن شبکه را ایجاد یا حذف نمیکند. اگر مرحله ایجاد را نادیده بگیرید، Compose از شروع کار خودداری میکند و گزارش میدهد که شبکه بهعنوان خارجی تعریف شده، اما پیدا نشده است.
چرا وقتی UFW پورت را مسدود کرده است، کانتینر من از اینترنت قابل دسترسی است؟
زیرا پورت publishشده با قوانین forwardingای مدیریت میشود که Docker به iptables اضافه میکند. این قوانین پیش از قوانین UFW بررسی میشوند و ترافیک forwarded نیز از chainای که UFW فیلتر میکند عبور نمیکند. سمت میزبان را با "127.0.0.1:8080:80" به loopback متصل کنید و هر سرویس عمومی را پشت یک reverse proxy روی پورتهای 80 و 443 قرار دهید.