توی این درس یاد میگیری Nginx را بهجای نصب روی سرور، با ایمیج رسمی داکر (nginx:alpine) اجرا کنی و جلوی سرویسهای دیگر یک پروژهی Compose بگذاری. تنظیمات و فایلهای سایت را mount میکنی (و میبینی چرا mount یک فایل تنها دردسر میسازد)، سرویسها را با اسمشان صدا میزنی (proxy_pass http://api:8080)، تنظیمات را بدون ریستارت با docker compose exec nginx nginx -s reload اعمال میکنی، و دو خطای معروف داکری را از نزدیک میبینی: host not found in upstream و 502 بعد از عوض شدن IP یک کانتینر.
مسئله: همهچیز در داکر است، بهجز Nginx
Section titled “مسئله: همهچیز در داکر است، بهجز Nginx”اگر دورهی داکر را دیده باشی، اپهایت احتمالاً در کانتینرند: API، فرانت، دیتابیس، همه با یک compose.yaml. حالا Nginx را کجا بگذاری؟
- روی خود سرور (مثل درسهای قبل): Nginx باید به پورتهایی که کانتینرها روی میزبان باز کردهاند وصل شود؛ پس هر سرویس یک پورت روی میزبان لازم دارد، و نسخه و تنظیمات Nginx جدا از پروژه مدیریت میشود.
- داخل همان Compose: Nginx یک سرویس کنار بقیه است، سرویسها را با اسم صدا میزند، فقط خودش پورت به بیرون دارد، و کل پروژه (با تنظیمات Nginx) با یک
docker compose upروی هر ماشینی بالا میآید.
این درس راه دوم است. همهی چیزهایی که دربارهی تنظیمات Nginx یاد گرفتی همان است؛ فقط چند تفاوت داکری دارد که اگر ندانی، ساعتها وقتت را میگیرد.
تشبیه: پذیرش یک ساختمان اداری
Section titled “تشبیه: پذیرش یک ساختمان اداری”Compose مثل یک ساختمان اداری است و هر سرویس یک دفتر با اسم روی در (web، api). Nginx میز پذیرش طبقهی همکف است: تنها در ورودی ساختمان (تنها پورت باز به بیرون). مراجع میگوید «با حسابداری کار دارم» و پذیرش او را به دفتر api میفرستد. پذیرش برای پیدا کردن دفترها از راهنمای ساختمان (DNS داخلی داکر) استفاده میکند. مشکل وقتی است که پذیرش شمارهی اتاق را صبح یک بار از راهنما بپرسد و روی کاغذ بنویسد؛ اگر حسابداری ظهر به اتاق دیگری برود، پذیرش مراجعها را به اتاق قدیمی میفرستد.
ایمیج رسمی Nginx در یک نگاه
Section titled “ایمیج رسمی Nginx در یک نگاه”قبل از ساختن، ببینیم داخل nginx:alpine چه خبر است:
docker run --rm nginx:alpine nginx -vdocker run --rm nginx:alpine sh -c 'ls /etc/nginx/conf.d; grep -n "include\|error_log\|access_log" /etc/nginx/nginx.conf; ls -l /var/log/nginx'/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d//docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh10-listen-on-ipv6-by-default.sh: info: ipv6 not available/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh/docker-entrypoint.sh: Configuration complete; ready for start upnginx version: nginx/1.31.6default.conf5:error_log /var/log/nginx/error.log notice;15: include /etc/nginx/mime.types;22: access_log /var/log/nginx/access.log main;31: include /etc/nginx/conf.d/*.conf;total 0lrwxrwxrwx 1 root root 11 Sep 22 21:19 access.log -> /dev/stdoutlrwxrwxrwx 1 root root 11 Sep 22 21:19 error.log -> /dev/stderrچند چیز مهم:
- اسکریپتهای شروع: هر بار که کانتینر با دستور
nginxشروع میشود،docker-entrypoint.shاسکریپتهای پوشهی/docker-entrypoint.d/را اجرا میکند: فعال کردن IPv6 (اینجا نبود)، پردازش قالبها با envsubst (مثال ۷) و تنظیم تعداد workerها. خطهای بالای خروجی همینها هستند. - نسخه:
nginx/1.31.6، خیلی جدیدتر از ۱.۲۴ Ubuntu. یکی از مزیتهای ایمیج رسمی همین است. include /etc/nginx/conf.d/*.conf;: این ایمیجsites-availableوsites-enabledندارد. تنظیمات سایتها درconf.dاست که فعلاً فقطdefault.confدارد.- لاگها:
access.logوerror.logدر واقع لینک به/dev/stdoutو/dev/stderrهستند. پس Nginx در فایل نمینویسد و همهچیز باdocker logsدیده میشود (روش استاندارد داکر).
مثالهای عملی
Section titled “مثالهای عملی”این درس روی خود داکر اجرا شده (Docker Compose v5، ایمیج nginx:alpine). همهی پورتها فقط روی 127.0.0.1 باز شدهاند و کانتینرها پیشوند lx- دارند.
مثال ۱: یک سایت استاتیک با mount تنظیمات
Section titled “مثال ۱: یک سایت استاتیک با mount تنظیمات”یک پوشه برای سایت و یک پوشه برای تنظیمات Nginx:
<h1>Hello from Nginx in Docker</h1>server { listen 80; server_name _; root /usr/share/nginx/html;
location / { try_files $uri $uri/ =404; }}docker run -d --name lx-ngx -p 127.0.0.1:8099:80 \ -v ./conf:/etc/nginx/conf.d:ro \ -v ./site:/usr/share/nginx/html:ro \ nginx:alpinesleep 1curl -s http://127.0.0.1:8099/curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8099/nopedocker logs lx-ngx 2>&1 | tail -n 222f0707ce013dbd9d8aec4a519ca77b12c1329a3dfdafecd69477b7b4fdb6a18<h1>Hello from Nginx in Docker</h1>404172.17.0.1 - - [04/Oct/2026:14:23:44 +0000] "GET / HTTP/1.1" 200 36 "-" "curl/8.5.0" "-"172.17.0.1 - - [04/Oct/2026:14:23:44 +0000] "GET /nope HTTP/1.1" 404 153 "-" "curl/8.5.0" "-"-v ./conf:/etc/nginx/conf.d:roکل پوشهیconfما را جایconf.dایمیج گذاشت (پسdefault.confخود ایمیج کنار رفت و مال ما جایش نشست).:roیعنی فقط خواندنی؛ کانتینر نمیتواند تنظیمات را عوض کند.-v ./site:/usr/share/nginx/html:roفایلهای سایت.-p 127.0.0.1:8099:80پورت ۸۰ کانتینر را فقط روی127.0.0.1:8099میزبان باز کرد.- خط اول خروجی شناسهی کانتینر است. صفحه آمد،
/nope404شد، وdocker logsهر دو درخواست را نشان داد. IP کاربر172.17.0.1است: درگاه شبکهی پیشفرض داکر، چون درخواست از میزبان آمده و از NAT داکر رد شده.
مثال ۲: تست و reload داخل کانتینر
Section titled “مثال ۲: تست و reload داخل کانتینر”تنظیمات را روی میزبان عوض میکنیم (یک هدر اضافه)، داخل کانتینر تستش میکنیم و reload میکنیم:
sed -i 's|^ root /usr/share/nginx/html;| root /usr/share/nginx/html;\n add_header X-Served-By docker-nginx always;|' conf/default.confdocker exec lx-ngx nginx -tdocker exec lx-ngx nginx -s reloadsleep 1curl -s -I http://127.0.0.1:8099/ | grep -i '^x-served-by'docker exec lx-ngx ps -o pid,args | grep -v psnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful2026/10/04 14:23:44 [notice] 30#30: signal process startedX-Served-By: docker-nginxPID COMMAND 1 nginx: master process nginx -g daemon off; 36 nginx: worker process 37 nginx: worker process 38 nginx: worker process 39 nginx: worker processdocker exec lx-ngx nginx -tهمانnginx -tهمیشگی است، ولی داخل کانتینر و با فایلهایی که کانتینر میبیند. (فایل را روی میزبان عوض کردیم؛ چون پوشه mount شده، کانتینر فوراً نسخهی جدید را میبیند.)nginx -s reloadبه پروسهی master سیگنال reload میفرستد (همان کارsystemctl reloadروی سرور). هدر جدید آمد.ps: در کانتینر، master Nginx PID 1 است (باdaemon off;، یعنی در پیشزمینه میماند تا کانتینر زنده بماند) و ۴ worker دارد. نه systemd هست و نهsystemctl؛ مدیریت باdockerو سیگنالهای خود Nginx است.
مثال ۳: Nginx جلوی دو سرویس Compose
Section titled “مثال ۳: Nginx جلوی دو سرویس Compose”حالا تمرین اصلی درس: یک پروژهی Compose با سه سرویس. web (فایلهای استاتیک، با همان ایمیج nginx:alpine) و api (یک برنامهی کوچک Go که JSON برمیگرداند)، و جلوی هر دو، nginx.
API (اگر Go بلد نیستی، نگران نباش: فقط اسم کانتینر و مسیر درخواست را بهصورت JSON برمیگرداند):
// tiny JSON API: reports which container answeredpackage main
import ( "encoding/json" "net/http" "os")
func main() { host, _ := os.Hostname() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "service": "api", "container": host, "path": r.URL.Path, "real_ip": r.Header.Get("X-Real-IP"), }) }) http.ListenAndServe(":8080", nil)}# build stage: compile a static binaryFROM golang:1.24-alpine AS buildWORKDIR /srcCOPY main.go .RUN CGO_ENABLED=0 go build -o /api main.go
# run stage: just the binaryFROM scratchCOPY --from=build /api /apiEXPOSE 8080ENTRYPOINT ["/api"]تنظیمات Nginx. بهجای IP، اسم سرویسها:
server { listen 80; server_name _;
location / { proxy_pass http://web:80; }
location /api/ { proxy_pass http://api:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}services: nginx: image: nginx:alpine ports: - "127.0.0.1:8099:80" volumes: - ./nginx:/etc/nginx/conf.d:ro depends_on: - web - api
web: image: nginx:alpine volumes: - ./site:/usr/share/nginx/html:ro
api: build: ./api image: lx-nd-apidocker rm -f lx-ngx > /dev/nulldocker compose -p lx-nd build --quiet apidocker compose -p lx-nd up -d 2> /dev/nullsleep 1docker compose -p lx-nd ps --format '{{.Service}}\t{{.Status}}\t{{.Ports}}'docker images lx-nd-api --format '{{.Repository}} {{.Size}}'echo "=== /"curl -s http://127.0.0.1:8099/echo "=== /api/users"curl -s http://127.0.0.1:8099/api/users Image lx-nd-api Building Image lx-nd-api Builtapi Up 1 second 8080/tcpnginx Up 1 second 127.0.0.1:8099->80/tcpweb Up 1 second 80/tcplx-nd-api 13.2MB=== /<h1>Hello from Nginx in Docker</h1>=== /api/users{"container":"a53aa58b7c63","path":"/api/users","real_ip":"172.18.0.1","service":"api"}- فقط
nginxپورت روی میزبان دارد (127.0.0.1:8099->80/tcp).webوapiفقط داخل شبکهی Compose در دسترساند (80/tcpو8080/tcpبدون فلش). یعنی API مستقیم از اینترنت قابل دسترس نیست؛ همه از Nginx رد میشوند. - ایمیج API فقط ۱۳٫۲ مگابایت است: build چندمرحلهای (درس multi-stage در دورهی داکر) و
FROM scratch(هیچ سیستمعاملی، فقط فایل اجرایی Go). /بهwebرفت و/api/usersبهapi. درproxy_pass http://api:8080;،apiاسم سرویس درcompose.yamlاست؛ Compose یک شبکه برای پروژه میسازد و داخل آن اسم هر سرویس به IP کانتینرش ترجمه میشود.- API هدر
X-Real-IPرا172.18.0.1دید: درگاه شبکهی Compose (این بار شبکهی پروژه172.18.0.0/16است، نه شبکهی پیشفرض).
مثال ۴: docker compose exec nginx nginx -s reload
Section titled “مثال ۴: docker compose exec nginx nginx -s reload”یک تغییر در تنظیمات: پاسخهای API نباید کش شوند، و یک location برای سلامت (/healthz) که خود Nginx جواب بدهد. بعد reload، بدون ریستارت کانتینر:
sed -i 's|^ location /api/ {| location = /healthz {\n default_type text/plain;\n return 200 "ok\\n";\n }\n\n location /api/ {\n add_header Cache-Control "no-store" always;|' nginx/default.confstarted=$(docker inspect -f '{{.State.StartedAt}}' lx-nd-nginx-1)docker compose -p lx-nd exec nginx nginx -tdocker compose -p lx-nd exec nginx nginx -s reloadsleep 1curl -s http://127.0.0.1:8099/healthzcurl -s -I http://127.0.0.1:8099/api/x | grep -i '^cache-control'[ "$started" = "$(docker inspect -f '{{.State.StartedAt}}' lx-nd-nginx-1)" ] && echo "same container, not restarted"nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful2026/10/04 14:23:48 [notice] 30#30: signal process startedokCache-Control: no-storesame container, not restartedاین همان دستور اصلی درس است: docker compose exec nginx nginx -s reload. exec یک دستور را داخل کانتینرِ در حال اجرای سرویس nginx اجرا میکند. location /healthz و هدر Cache-Control اعمال شدند، و زمان شروع کانتینر (StartedAt) عوض نشد: کانتینر ریستارت نشده و هیچ اتصالی قطع نشده. (اگر docker compose restart nginx میزدی، کانتینر کامل متوقف و دوباره اجرا میشد و درخواستهای در جریان قطع میشدند.)
مثال ۵: IP کانتینر عوض شد، Nginx خبر نداشت
Section titled “مثال ۵: IP کانتینر عوض شد، Nginx خبر نداشت”حالا سناریوی اول درس را بازسازی کنیم. API را متوقف میکنیم، یک کانتینر دیگر در همان شبکه بالا میآید (و IP آزاد شده را میگیرد)، و API دوباره شروع میشود؛ با یک IP جدید:
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }echo "api before: $(ip lx-nd-api-1)"docker compose -p lx-nd stop api 2> /dev/nulldocker compose -p lx-nd run -d --rm --name lx-nd-tmp web > /dev/null 2>&1docker compose -p lx-nd start api 2> /dev/nullecho "api after: $(ip lx-nd-api-1) (old IP now belongs to lx-nd-tmp: $(ip lx-nd-tmp))"docker compose -p lx-nd exec nginx getent hosts apicurl -s -o /dev/null -w 'GET /api/users -> %{http_code}\n' http://127.0.0.1:8099/api/usersdocker compose -p lx-nd logs nginx 2>&1 | grep -o 'connect() failed.*upstream: "[^"]*"' | tail -n 1 | sed 's/ while connecting.*upstream:/ ... upstream:/'api before: 172.18.0.2api after: 172.18.0.5 (old IP now belongs to lx-nd-tmp: 172.18.0.2)172.18.0.5 api apiGET /api/users -> 502connect() failed (111: Connection refused) ... upstream: "http://172.18.0.2:8080/api/users"خطبهخط:
- API از IP قبلی به IP جدید رفت، و IP قبلی را حالا یک کانتینر دیگر (
lx-nd-tmp، یک نسخهیweb) دارد. getent hosts apiداخل کانتینر Nginx: DNS داکر درست جواب میدهد و IP جدید را میگوید.- ولی Nginx
502داد و لاگش میگوید به IP قدیمی وصل شده (و آنجا کسی روی ۸۰۸۰ گوش نمیداد:Connection refused).
چرا؟ Nginx اسمهایی که در proxy_pass (یا server یک upstream) نوشته شدهاند را فقط یک بار، موقع شروع یا reload، resolve میکند و IP را تا reload بعدی نگه میدارد. این همان معمای سناریوی اول درس است: بعد از دیپلوی API، کانتینر جدید IP جدید گرفته بود. یک nginx -s reload درستش میکرد، چون اسم را دوباره resolve میکرد. بدتر از 502 هم ممکن است: اگر کانتینر صاحب IP قدیمی روی همان پورت گوش بدهد، درخواستها به سرویس اشتباه میروند.
مثال ۶: راهحل، resolver و resolve
Section titled “مثال ۶: راهحل، resolver و resolve”راهحل این است که Nginx اسم را هر چند ثانیه دوباره از DNS داکر بپرسد. همان کاری که در درس load balancing برای scale کردن کردیم: resolver 127.0.0.11 (آدرس DNS داخلی داکر در هر کانتینر) و یک upstream با پارامتر resolve (Nginx ۱.۲۷.۳ به بعد؛ ایمیج ما ۱.۳۱ است):
cat > nginx/default.conf <<'EOF'# Docker's embedded DNS; re-resolve names every 5sresolver 127.0.0.11 valid=5s;
upstream api { zone api 64k; server api:8080 resolve;}
server { listen 80; server_name _;
location / { proxy_pass http://web:80; }
location /api/ { proxy_pass http://api; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}EOFdocker compose -p lx-nd exec nginx nginx -t 2>&1 | tail -n 1docker compose -p lx-nd exec nginx nginx -s reloadsleep 1curl -s -o /dev/null -w 'after reload: %{http_code}\n' http://127.0.0.1:8099/api/usersecho "=== move api to a new IP again:"echo "api before: $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lx-nd-api-1)"docker compose -p lx-nd stop api 2> /dev/nulldocker compose -p lx-nd run -d --rm --name lx-nd-tmp2 web > /dev/null 2>&1docker compose -p lx-nd start api 2> /dev/nullecho "api after: $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lx-nd-api-1)"curl -s -o /dev/null -w 'no reload, right away: %{http_code}\n' http://127.0.0.1:8099/api/userssleep 6curl -s -o /dev/null -w 'no reload, 6s later: %{http_code}\n' http://127.0.0.1:8099/api/usersdocker rm -f lx-nd-tmp lx-nd-tmp2 > /dev/nullnginx: configuration file /etc/nginx/nginx.conf test is successful2026/10/04 14:23:50 [notice] 52#52: signal process startedafter reload: 200=== move api to a new IP again:api before: 172.18.0.5api after: 172.18.0.6no reload, right away: 502no reload, 6s later: 200- بعد از reload (با اسم دوباره resolve شد) API دوباره
200داد. - API را دوباره جابهجا کردیم (IP جدید). بلافاصله هنوز
502، چون Nginx تا ۵ ثانیه (valid=5s) جواب قبلی DNS را معتبر میداند. - ۶ ثانیه بعد، بدون هیچ reload ای:
200. Nginx اسم را دوباره پرسید و عضو upstream را بهروز کرد.
valid را کوتاه بگذار (چند ثانیه)؛ پرسیدن از DNS محلی داکر تقریباً هزینهای ندارد. در Nginx قدیمیتر از ۱.۲۷.۳ (مثل ۱.۲۴ Ubuntu) resolve نیست. آنجا ترفند رایج این است که اسم را در یک متغیر بگذاری (set $api http://api:8080; proxy_pass $api;)؛ وقتی proxy_pass متغیر دارد، Nginx در هر درخواست با resolver اسم را resolve میکند. (ولی دقت کن: با متغیر، رفتار مسیر هم فرق میکند و upstream و keepalive هم از دست میرود.)
مثال ۷: قالبها با متغیر محیطی (envsubst)
Section titled “مثال ۷: قالبها با متغیر محیطی (envsubst)”گاهی یک تنظیم باید در محیطهای مختلف فرق کند (مثلاً اسم دامنه، یا آدرس API). ایمیج رسمی Nginx یک امکان داخلی دارد: هر فایل *.template در /etc/nginx/templates/، موقع شروع کانتینر با envsubst پردازش میشود (متغیرهای ${NAME} با مقدار متغیر محیطی جایگزین میشوند) و نتیجه در /etc/nginx/conf.d/ نوشته میشود:
server { listen 80; server_name ${SITE_NAME};
location / { default_type text/plain; return 200 "site: ${SITE_NAME}, env: ${APP_ENV}\n"; }}docker run -d --name lx-ngx-tpl -p 127.0.0.1:8098:80 \ -e SITE_NAME=shop.example -e APP_ENV=staging \ -v ./templates:/etc/nginx/templates:ro \ nginx:alpinesleep 1curl -s http://127.0.0.1:8098/docker logs lx-ngx-tpl 2>&1 | grep -i templatedocker exec lx-ngx-tpl head -n 3 /etc/nginx/conf.d/default.confdocker rm -f lx-ngx-tpl > /dev/nulle77d116ceaf248b9c9077d9ff2cdeabed31f7435779066747ce3758d5fb7e7c1site: shop.example, env: staging/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh20-envsubst-on-templates.sh: Running envsubst on /etc/nginx/templates/default.conf.template to /etc/nginx/conf.d/default.confserver { listen 80; server_name shop.example;-e SITE_NAME=... -e APP_ENV=...متغیرهای محیطی کانتینر.- اسکریپت
20-envsubst-on-templates.sh(همان که در اول درس دیدیم) فایلdefault.conf.templateرا خواند،${SITE_NAME}و${APP_ENV}را با مقدارشان جایگزین کرد و نتیجه را در/etc/nginx/conf.d/default.confنوشت. - پس یک فایل تنظیمات داری که در staging و production با متغیرهای مختلف اجرا میشود.
دو نکته: ۱) این پردازش فقط موقع شروع کانتینر است؛ تغییر متغیر یعنی ساختن دوبارهی کانتینر (docker compose up -d)، نه reload. ۲) متغیرهای خود Nginx (مثل $host و $remote_addr) هم شبیه همیناند! envsubst فقط متغیرهایی را جایگزین میکند که در محیط کانتینر تعریف شدهاند، پس $host دستنخورده میماند؛ ولی اگر روزی متغیر محیطیای به اسم host تعریف کنی، تنظیمت خراب میشود. (با متغیر NGINX_ENVSUBST_FILTER میشود فقط متغیرهای با پیشوند خاص را جایگزین کرد.)
پشت پرده: DNS داکر و «یک بار resolve»
Section titled “پشت پرده: DNS داکر و «یک بار resolve»”هر کانتینر در یک شبکهی Compose، بهجای DNS میزبان از سرور DNS داخلی داکر روی آدرس جادویی 127.0.0.11 استفاده میکند. این سرور اسم سرویسها (و اسم کانتینرها) را به IP های فعلیشان ترجمه میکند:
docker compose -p lx-nd exec nginx cat /etc/resolv.conf | grep -v '^#'docker compose -p lx-nd exec nginx getent hosts web apidocker compose -p lx-nd exec nginx nginx -T 2> /dev/null | grep -n 'resolver\|server api'nameserver 127.0.0.11options timeout:2 attempts:3 ndots:0
172.18.0.3 web web172.18.0.6 api api138:resolver 127.0.0.11 valid=5s;142: server api:8080 resolve;/etc/resolv.confکانتینر:nameserver 127.0.0.11. این آدرس، سرور DNS داخلی داکر است که داخل هر کانتینرِ یک شبکهی سفارشی (مثل شبکهی Compose) در دسترس است. (در شبکهی پیشفرضbridge، کهdocker runساده به آن وصل میشود، اسمگذاری سرویسها کار نمیکند؛ برای همین Compose برای هر پروژه شبکهی جدا میسازد.)getent hostsهر دو سرویس را با IP فعلیشان نشان میدهد.nginx -Tکل تنظیمات نهایی را (بعد از همهی include ها) چاپ میکند؛ باgrepدیدیمresolverوserver api:8080 resolveواقعاً در تنظیمات در حال اجرا هستند.
خلاصهی رفتار Nginx با اسمها:
| نوشته | کی resolve میشود | اگر موقع شروع اسم نباشد |
|---|---|---|
proxy_pass http://api:8080; |
یک بار، موقع شروع یا reload | Nginx شروع نمیشود (host not found in upstream) |
upstream { server api:8080; } |
یک بار، موقع شروع یا reload | Nginx شروع نمیشود |
upstream { zone ...; server api:8080 resolve; } + resolver |
هر valid ثانیه |
شروع میشود؛ تا آمدن سرویس ۵۰۲ |
set $u http://api:8080; proxy_pass $u; + resolver |
در هر درخواست (با کش valid) |
شروع میشود؛ تا آمدن سرویس ۵۰۲ |
جدول مرجع
Section titled “جدول مرجع”مسیرهای مهم در ایمیج رسمی:
| مسیر | چیست |
|---|---|
/etc/nginx/nginx.conf |
تنظیم اصلی (include /etc/nginx/conf.d/*.conf;) |
/etc/nginx/conf.d/default.conf |
سایت پیشفرض؛ معمولاً کل پوشه را با تنظیمات خودت mount میکنی |
/etc/nginx/templates/*.template |
قالبهای envsubst، نتیجه در conf.d |
/usr/share/nginx/html |
ریشهی سایت پیشفرض |
/var/log/nginx/access.log و error.log |
لینک به /dev/stdout و /dev/stderr (پس docker logs) |
دستورها:
| دستور | کار |
|---|---|
docker compose exec nginx nginx -t |
تست تنظیمات داخل کانتینر |
docker compose exec nginx nginx -s reload |
اعمال تنظیمات بدون ریستارت |
docker compose exec nginx nginx -T |
چاپ کل تنظیمات نهایی (با همهی include ها) |
docker compose logs -f nginx |
دنبال کردن لاگها |
docker compose exec nginx getent hosts api |
IP فعلی یک سرویس از دید Nginx |
docker compose restart nginx |
ریستارت کامل (اتصالهای جاری قطع میشوند) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) mount یک فایل تنها، و ویرایش با sed -i
Section titled “۱) mount یک فایل تنها، و ویرایش با sed -i”اگر بهجای پوشه، یک فایل را mount کنی، داکر در واقع آن inode (شناسهی فایل روی دیسک) را به کانتینر وصل میکند. خیلی از ویرایشگرها و sed -i فایل را عوض نمیکنند، بلکه یک فایل جدید با همان اسم میسازند:
echo 'version one' > single.confdocker run -d --name lx-ngx-file -v ./single.conf:/etc/single.conf:ro nginx:alpine > /dev/nulldocker exec lx-ngx-file cat /etc/single.confls -i single.conf | cut -d' ' -f1sed -i 's/one/two/' single.confls -i single.conf | cut -d' ' -f1cat single.confdocker exec lx-ngx-file cat /etc/single.confdocker rm -f lx-ngx-file > /dev/nullversion one19254131925405version twoversion onesed -i یک فایل جدید ساخت (دو عدد وسط خروجی، شمارهی inode قبل و بعد از sed -i هستند و فرق دارند) و قدیمی را پاک کرد. میزبان version two را میبیند، ولی کانتینر هنوز به inode قدیمی وصل است و version one را میبیند؛ و هیچ reload ای درستش نمیکند، چون از دید کانتینر فایل اصلاً عوض نشده. فقط ساختن دوبارهی کانتینر (یا ریستارتش) mount را تازه میکند. خیلی از ویرایشگرها (مثل vim با تنظیمات پیشفرض بعضی سیستمها) هم همین کار را میکنند. راهحل: همیشه پوشه را mount کن (مثل ./nginx:/etc/nginx/conf.d)، نه فایل تنها.
۲) host not found in upstream
Section titled “۲) host not found in upstream”docker compose -p lx-nd stop api 2> /dev/nulldocker compose -p lx-nd rm -f api > /dev/null 2>&1mkdir -p old-styleprintf 'server {\n listen 80;\n location / { proxy_pass http://api:8080; }\n}\n' > old-style/default.confdocker run -d --name lx-ngx-old --network lx-nd_default -v ./old-style:/etc/nginx/conf.d:ro nginx:alpine > /dev/nullsleep 1docker ps -a --filter name=lx-ngx-old --format '{{.Names}}: {{.Status}}'docker logs lx-ngx-old 2>&1 | grep emerg | tail -n 1docker rm -f lx-ngx-old > /dev/nullecho "=== with resolve (our nginx service), api still gone:"curl -s -o /dev/null -w '/api/ -> %{http_code}, ' http://127.0.0.1:8099/api/userscurl -s -o /dev/null -w '/ -> %{http_code}\n' http://127.0.0.1:8099/docker compose -p lx-nd up -d api 2> /dev/nullsleep 6curl -s -o /dev/null -w 'api back: /api/ -> %{http_code}\n' http://127.0.0.1:8099/api/userslx-ngx-old: Exited (1) 1 second agonginx: [emerg] host not found in upstream "api" in /etc/nginx/conf.d/default.conf:3=== with resolve (our nginx service), api still gone:/api/ -> 502, / -> 200api back: /api/ -> 200API را کامل حذف کردیم و یک Nginx با تنظیم «قدیمی» (proxy_pass http://api:8080; بدون resolve) در همان شبکه اجرا کردیم: Exited (1) با پیام host not found in upstream "api". Nginx نتوانست اسم را موقع شروع resolve کند و کلاً بالا نیامد؛ یعنی کل سایت (حتی بخشهایی که به API ربطی ندارند) به خاطر یک سرویس پایین است. depends_on هم فقط ترتیب شروع را تضمین میکند، نه اینکه سرویس همیشه باشد.
سرویس nginx پروژهی ما (با resolve) در همان وضعیت: /api/ 502 ولی / 200؛ یعنی فقط بخش خراب از دسترس خارج شد. وقتی API برگشت، چند ثانیه بعد /api/ خودش درست شد.
۳) localhost داخل کانتینر
Section titled “۳) localhost داخل کانتینر”sed 's|proxy_pass http://web:80;|proxy_pass http://localhost:8080;|' nginx/default.conf > nginx/default.conf.newmv nginx/default.conf nginx/default.conf.goodmv nginx/default.conf.new nginx/default.confdocker compose -p lx-nd exec nginx nginx -s reloadsleep 1curl -s -o /dev/null -w '/ -> %{http_code}\n' http://127.0.0.1:8099/docker compose -p lx-nd logs nginx 2>&1 | grep -o 'connect() failed[^,]*' | tail -n 1mv nginx/default.conf.good nginx/default.confdocker compose -p lx-nd exec nginx nginx -s reload2026/10/04 14:24:09 [notice] 81#81: signal process started/ -> 502connect() failed (111: Connection refused) while connecting to upstream2026/10/04 14:24:11 [notice] 91#91: signal process started/ با 502 و Connection refused. داخل کانتینر، localhost (یا 127.0.0.1) یعنی خود کانتینر Nginx، نه میزبان و نه کانتینرهای دیگر. هر کانتینر شبکهی جدای خودش را دارد. این اشتباه معمولاً وقتی پیش میآید که تنظیمات Nginx روی سرور را بدون تغییر به داکر میآوری. راهحل: اسم سرویس (http://web:80). (برای رسیدن به سرویسی روی خود میزبان، در لینوکس extra_hosts: ["host.docker.internal:host-gateway"] در Compose و بعد http://host.docker.internal:PORT.)
۴) reload با تنظیم خراب
Section titled “۴) reload با تنظیم خراب”cp nginx/default.conf nginx/default.conf.goodsed -i 's|proxy_pass http://web:80;|proxy_pass http://web:80|' nginx/default.confdocker compose -p lx-nd exec nginx nginx -s reload; echo "exit code: $?"sleep 1curl -s -o /dev/null -w 'site still up: %{http_code}\n' http://127.0.0.1:8099/cp nginx/default.conf.good nginx/default.confdocker compose -p lx-nd exec nginx nginx -s reload2026/10/04 14:24:11 [emerg] 101#101: unexpected "}" in /etc/nginx/conf.d/default.conf:15nginx: [emerg] unexpected "}" in /etc/nginx/conf.d/default.conf:15exit code: 1site still up: 2002026/10/04 14:24:12 [notice] 107#107: signal process startedیک ; جا افتاد. nginx -s reload خطا داد و کد خروج ۱، ولی سایت همچنان 200 است: Nginx با تنظیم قبلی ادامه داد. این بار خوششانس بودیم چون خطای syntax در همان دستور reload دیده شد؛ ولی خطاهایی که فقط موقع اعمال پیدا میشوند (مثل عوض شدن کلید یک zone در درس محدودسازی) فقط در لاگ ظاهر میشوند. راهحل: همیشه nginx -t قبل از reload، و نگاه به docker compose logs nginx بعد از آن (تمرین سخت همین را خودکار میکند).
در پروژهی مثال ۳، متن صفحهی اصلی (site/index.html) را عوض کن. آیا برای دیدن تغییر، reload یا ریستارت لازم است؟ آزمایش کن.
دیدن جواب
echo '<h1>Hello again, nothing restarted</h1>' > site/index.htmlcurl -s http://127.0.0.1:8099/<h1>Hello again, nothing restarted</h1>نه. فایلهای سایت با یک پوشه mount شدهاند و Nginx هر فایل را موقع درخواست از دیسک میخواند؛ پس تغییر فوراً دیده میشود. reload فقط برای تنظیمات Nginx لازم است. (و چون پوشه mount شده، نه فایل تنها، دام اشتباه ۱ هم پیش نمیآید: echo > محتوای همان فایل را عوض میکند و حتی ویرایشگری که فایل جدید بسازد هم درون پوشهی mount شده دیده میشود.)
تمرین اصلی درس، نسخهی دو دامنه: همان دو سرویس (web و api) را با دو اسم دامنه پشت Nginx بگذار: www.lx.test به web و api.lx.test به api. با curl --resolve (بدون دست زدن به /etc/hosts) نشان بده هر دامنه به سرویس درست میرسد و دامنهی ناشناخته 444 (بستن اتصال بدون جواب) میگیرد.
دیدن جواب
cp nginx/default.conf nginx/default.conf.goodcat > nginx/default.conf <<'EOF'resolver 127.0.0.11 valid=5s;
upstream api { zone api 64k; server api:8080 resolve;}
# unknown names: close the connection without a responseserver { listen 80 default_server; return 444;}
server { listen 80; server_name www.lx.test; location / { proxy_pass http://web:80; }}
server { listen 80; server_name api.lx.test; location / { proxy_pass http://api; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}EOFdocker compose -p lx-nd exec nginx nginx -t 2>&1 | tail -n 1docker compose -p lx-nd exec nginx nginx -s reloadsleep 1for h in www.lx.test api.lx.test other.lx.test; do printf '%-14s -> ' "$h" curl -s --resolve "$h:8099:127.0.0.1" "http://$h:8099/" -w ' [%{http_code}]\n'donecp nginx/default.conf.good nginx/default.confdocker compose -p lx-nd exec nginx nginx -s reloadnginx: configuration file /etc/nginx/nginx.conf test is successful2026/10/04 14:24:12 [notice] 123#123: signal process startedwww.lx.test -> <h1>Hello again, nothing restarted</h1> [200]api.lx.test -> {"container":"b9aee8eb1fa8","path":"/","real_ip":"172.18.0.1","service":"api"} [200]other.lx.test -> [000]2026/10/04 14:24:14 [notice] 133#133: signal process startedwww.lx.testبهwebرسید (صفحهای که در تمرین قبل عوضش کردیم).api.lx.testبهapi(مسیر/).other.lx.testبهserverdefault_serverافتاد وreturn 444: Nginx اتصال را بدون هیچ پاسخی بست؛ برای همینcurlکد000(هیچ پاسخی) گزارش داد. این روش خوبی برای رد کردن درخواستهایی است که با IP یا دامنهی ناشناخته میآیند (رباتهای اسکن).curl --resolve HOST:PORT:IPبهcurlمیگوید این اسم را بدون DNS به این IP وصل کن؛ هم Host درست فرستاده میشود و هم به/etc/hostsدست نمیزنیم.
آخر سر تنظیم قبلی را برگرداندیم.
یک اسکریپت reload.sh بنویس که: ۱) تنظیمات را داخل کانتینر تست کند و اگر خراب بود، با پیام خطا و کد غیرصفر خارج شود و reload نکند؛ ۲) اگر سالم بود reload کند؛ ۳) دو ثانیه بعد، error log کانتینر (خروجی docker compose logs) را برای [emerg] تازه بگردد (یادت هست reload ناموفق بیصداست؟). با یک تنظیم سالم و یک تنظیم خراب آزمایشش کن.
دیدن جواب
#!/usr/bin/env bash# safe nginx reload for a Compose projectset -euo pipefailproject=${1:?usage: reload.sh PROJECT}dc() { docker compose -p "$project" "$@"; }
if ! out=$(dc exec -T nginx nginx -t 2>&1); then echo "config test FAILED, not reloading:" >&2 echo "$out" | grep emerg >&2 exit 1fisince=$(date -u +%Y-%m-%dT%H:%M:%SZ)dc exec -T nginx nginx -s reload 2> /dev/nullsleep 2if dc logs --since "$since" nginx 2>&1 | grep -q '\[emerg\]'; then echo "reload FAILED (old config still running):" >&2 dc logs --since "$since" nginx 2>&1 | grep '\[emerg\]' >&2 exit 1fiecho "reloaded OK"chmod +x reload.sh./reload.sh lx-nd; echo "exit: $?"cp nginx/default.conf nginx/default.conf.goodsed -i 's|server api:8080 resolve;|server api:8080 resolve weight=x;|' nginx/default.conf./reload.sh lx-nd; echo "exit: $?"cp nginx/default.conf.good nginx/default.conf./reload.sh lx-nd; echo "exit: $?"reloaded OKexit: 0config test FAILED, not reloading:2026/10/04 14:24:16 [emerg] 159#159: invalid parameter "weight=x" in /etc/nginx/conf.d/default.conf:6nginx: [emerg] invalid parameter "weight=x" in /etc/nginx/conf.d/default.conf:6exit: 1reloaded OKexit: 0سه اجرا:
- تنظیم سالم: تست، reload، و بعد از دو ثانیه هیچ
[emerg]تازهای در لاگ نبود:reloaded OKبا کد ۰. - تنظیم خراب (
weight=x):nginx -tشکست خورد، اسکریپت فقط خطemergرا نشان داد و reload نکرد (کد ۱). - تنظیم درست برگشت: دوباره OK.
نکتههای اسکریپت: exec -T (بدون ترمینال؛ برای اسکریپت و CI لازم است)، logs --since (فقط لاگهای بعد از reload، تا emerg های قدیمی اشتباه گزارش نشوند)، و set -euo pipefail از درس مدیریت خطا در Bash. این اسکریپت را میشود آخرین مرحلهی دیپلوی گذاشت.
آزمونک
Section titled “آزمونک”در کانتینر nginx نوشتهای proxy_pass http://localhost:3000 و اپ در سرویس app است. چه میشود؟
هر کانتینر localhost خودش را دارد.
docker logs روی کانتینر nginx چرا لاگ دسترسی را نشان میدهد؟
برای همین docker compose logs nginx کار میکند.
چرا mount پوشهی conf.d بهتر از mount یک فایل default.conf است؟
با mount پوشه، فایل تازه درون پوشه دیده میشود.
API بعد از دیپلوی IP تازه گرفته و Nginx ۵۰۲ میدهد تا reload. چرا؟
getent hosts api داخل کانتینر IP جدید را نشان میدهد ولی Nginx هنوز قدیمی را دارد.
کانتینر nginx با پیام host not found in upstream "api:8080" خارج میشود. علت؟
با resolve، Nginx بالا میآید و تا برگشتن api برای آن مسیر ۵۰۲ میدهد.
nginx -s reload با تنظیم خراب چه میکند؟
همیشه اول nginx -t.
فایلهای *.template در /etc/nginx/templates/ چه میشوند؟
فقط موقع شروع؛ تغییر متغیر یعنی ساختن دوبارهی کانتینر.
جمعبندی
Section titled “جمعبندی”- ایمیج رسمی:
nginx.confهمهیconf.d/*.confرا include میکند؛ پوشهیconf.dرا (نه یک فایل) و ریشهی سایت را با:romount کن. لاگها به stdout/stderr، پسdocker compose logs nginx. - Compose: فقط
nginxپورت منتشر میکند (127.0.0.1:...برای آزمایش)؛ بقیهی سرویسها با اسمشان (proxy_pass http://api:8080) و DNS داخلی داکر (127.0.0.11). - reload بدون ریستارت:
docker compose exec nginx nginx -tو بعدdocker compose exec nginx nginx -s reload. reload خراب بیصداست؛ لاگ را نگاه کن. - IP های متغیر: Nginx بدون
resolveاسمها را یک بار resolve میکند (۵۰۲ بعد از تغییر IP، وhost not found in upstreamاگر سرویس موقع شروع نباشد). راهحل:resolver 127.0.0.11 valid=5s;+upstream { zone ...; server api:8080 resolve; }. localhostداخل کانتینر یعنی خود کانتینر.- قالبها:
/etc/nginx/templates/*.template+ متغیر محیطی، با envsubst موقع شروع.
| دستور | کاری که میکند |
|---|---|
docker run -d -p 127.0.0.1:8099:80 -v ./conf:/etc/nginx/conf.d:ro nginx:alpine | Nginx با تنظیمات خودت |
docker exec CONTAINER nginx -t | تست تنظیمات |
docker compose exec nginx nginx -s reload | reload بدون ریستارت |
docker compose exec nginx nginx -T | کل تنظیمات نهایی |
docker compose logs nginx | لاگ دسترسی و خطا |
proxy_pass http://api:8080; | proxy به سرویس با اسم |
resolver 127.0.0.11 valid=5s; | DNS داخلی داکر |
server api:8080 resolve; | resolve دوبارهی اسم (در upstream با zone) |
docker compose exec nginx getent hosts api | IP فعلی سرویس |
-v ./templates:/etc/nginx/templates:ro -e NAME=... | قالب با envsubst |