- LoopX
- داکر از صفر تا حرفهای
- شبکه و Compose
- سرویسها، volumeها و شبکهها در Compose
سرویسها، volumeها و شبکهها در Compose
توی این درس یاد میگیری یک فایل Compose کامل بنویسی: هر سرویس را با image یا build تعریف کنی، ports و volumes (named و bind) بدهی، شبکههای سفارشی برای ایزولهسازی بسازی و restart policy بگذاری. با docker compose up --build یک پروژهی سهسرویسی (وب، اپ Flask و Redis) را از صفر بالا میآوری، میبینی داده با down میماند و با down -v میرود، و docker compose logs -f را برای دیدن لاگ زنده به کار میبری.
تشبیه: نقشهی یک ساختمان
Section titled “تشبیه: نقشهی یک ساختمان”فایل Compose مثل نقشهی ساختمان است: هر سرویس یک اتاق با مشخصات خودش (چه چیزی داخلش باشد: image/build)، درهای ورودی (ports)، انبار (volumes) و راهروهای مشترک (networks). بخشهای بالا (top-level) هم چیزهایی هستند که چند اتاق مشترکاً استفاده میکنند: انبارهای مشترک (volumes:) و راهروها (networks:).
ساختار فایل
Section titled “ساختار فایل”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: پروژهی سهسرویسی
Section titled “مثال ۱: پروژهی سهسرویسی”اول اپ Flask (یک شمارنده در Redis) و صفحهی وب:
import osfrom flask import Flask, jsonifyimport redis
app = Flask(__name__)app.json.ensure_ascii = Falser = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379)
@app.get("/")def hits(): return jsonify(hits=r.incr("hits"), message="شمارنده در Redis ذخیره میشود")flask==3.0.3gunicorn==22.0.0redis==5.0.8FROM python:3.12-slimENV PYTHONUNBUFFERED=1WORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]<!doctype html><meta charset="utf-8"><h1>صفحهی استاتیک (nginx)</h1>و فایل Compose:
name: lxsvc
services: web: image: nginx:alpine ports: - "8383:80" volumes: - ./web:/usr/share/nginx/html:ro networks: - frontend
app: build: ./app ports: - "8384:8000" environment: REDIS_HOST: redis networks: - frontend - backend
redis: image: redis:alpine volumes: - redis-data:/data networks: - backend restart: unless-stopped
networks: frontend: backend:
volumes: redis-data:بخش به بخش:
image:از Docker Hub؛build: ./appاز Dockerfile داخل پوشهی./appimage میسازد (معادلdocker build).ports:مثل-p؛volumes:هم bind mount (./web:/...:ro) و هم named volume (redis-data:/data).environment:متغیر محیطی؛ اینجا به اپ میگوید Redis کجاست (اسم سرویس).networks:کدام شبکهها؛ بخش top-levelnetworks:وvolumes:آنها را تعریف میکند (بدون تعریف، خطا میگیری).restart: unless-stopped: اگر کانتینر بیفتد، خودکار دوباره بالا بیاید (مگر خودت متوقفش کرده باشی).
cd shopdocker compose up -d --build 2>&1 | grep -E "Built|Started|Created|Healthy" | awk '!s[$0]++' | sed -E 's/^ +//' | head -8sleep 3docker compose ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}' | sed 's/, \[::\][^ ]*//'Image lxsvc-app BuiltVolume lxsvc_redis-data CreatedNetwork lxsvc_frontend CreatedNetwork lxsvc_backend CreatedContainer lxsvc-app-1 CreatedContainer lxsvc-redis-1 CreatedContainer lxsvc-web-1 CreatedContainer lxsvc-app-1 StartedSERVICE STATUS PORTSapp Up 4 seconds 0.0.0.0:8384->8000/tcpredis Up 4 seconds 6379/tcpweb Up 3 seconds 0.0.0.0:8383->80/tcp--build یعنی image سرویسهایی که build: دارند اول ساخته (یا بهروز) شوند. بعد همهی سه سرویس بالا آمدند. حالا امتحان:
for i in 1 2 3; do curl -s localhost:8384/; echo; donecurl -s localhost:8383/ | head -1{"hits":1,"message":"شمارنده در Redis ذخیره میشود"}
{"hits":2,"message":"شمارنده در Redis ذخیره میشود"}
{"hits":3,"message":"شمارنده در Redis ذخیره میشود"}
<!doctype html><meta charset="utf-8"><h1>صفحهی استاتیک (nginx)</h1>شمارنده بالا میرود (۱، ۲، ۳) و صفحهی استاتیک هم جواب میدهد. اپ با اسم redis به Redis وصل شد (اسم سرویس = DNS).
کلید build: ./app در سرویس چه میکند؟
معادل docker build ./app؛ با up --build هر بار میتوانی دوباره بسازی.
مثال ۲: داده با down میماند، با down -v میرود
Section titled “مثال ۲: داده با down میماند، با down -v میرود”cd shopdocker compose down 2>&1 | grep -cE "Removed"docker compose up -d >/dev/null 2>&1; sleep 3echo "بعد از down و up دوباره: $(curl -s localhost:8384/)"docker compose down -v 2>&1 | grep -E "Volume" | head -1docker compose up -d >/dev/null 2>&1; sleep 3echo "بعد از down -v: $(curl -s localhost:8384/)"5بعد از down و up دوباره: {"hits":4,"message":"شمارنده در Redis ذخیره میشود"} Volume lxsvc_redis-data Removingبعد از down -v: {"hits":1,"message":"شمارنده در Redis ذخیره میشود"}بعد از down ساده، کانتینرها پاک شدند ولی volume redis-data ماند؛ شمارنده ادامه داشت (۴ و …). با down -v volume هم حذف شد و شمارنده از ۱ شروع شد. قاعده: down -v یعنی از دست دادن دادهی ماندگار.
مثال ۳: ایزولهسازی با شبکهها
Section titled “مثال ۳: ایزولهسازی با شبکهها”cd shopdocker compose exec -T app sh -c 'getent hosts redis | awk "{print \"app -> redis:\", \$1}"'docker compose exec -T web sh -c 'getent hosts redis || echo "web -> redis: resolve نمیشود"'docker inspect lxsvc-app-1 --format 'app روی: {{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'docker inspect lxsvc-web-1 --format 'web روی: {{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'app -> redis: 172.20.0.3web -> redis: resolve نمیشودapp روی: lxsvc_backend lxsvc_frontendweb روی: lxsvc_frontendاپ در هر دو شبکه است و Redis را میبیند؛ وب فقط در frontend است و Redis را نمیبیند. همین ایزولهسازی است که در سناریو خواستیم، و بدون هیچ دستور شبکهای: فقط با دو کلید در فایل.
مثال ۴: restart policy
Section titled “مثال ۴: restart policy”cd shopdocker inspect lxsvc-redis-1 --format 'قبل: restarts={{.RestartCount}} policy={{.HostConfig.RestartPolicy.Name}}'docker compose exec -T redis sh -c 'kill 1' 2>&1 | head -1sleep 6docker inspect lxsvc-redis-1 --format 'بعد: restarts={{.RestartCount}} status={{.State.Status}}'قبل: restarts=0 policy=unless-stoppedبعد: restarts=1 status=runningبا فرستادن سیگنال به پروسهی اصلی Redis (شبیه یک کرش)، کانتینر خودکار دوباره بالا آمد (RestartCount یک واحد زیاد شد). این به لطف restart: unless-stopped است. نکته: اگر خودت docker compose stop redis بزنی، دوباره بالا نمیآید؛ «unless-stopped» یعنی «مگر دستی متوقف شود».
مثال ۵: لاگها و بازسازی بعد از تغییر کد
Section titled “مثال ۵: لاگها و بازسازی بعد از تغییر کد”cd shopdocker compose logs --tail 3 app 2>&1 | sed -E 's/^[^|]+\| //' | cut -c1-90echo "# app.py را عوض میکنیم و فقط app دوباره ساخته میشود"echo "# تغییر" >> app/app.pydocker compose up -d --build 2>&1 | grep -E "Built|Recreate|Running|Started" | sed -E 's/^ +//' | awk '!s[$0]++' | head -6[2026-10-03 11:32:30 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)[2026-10-03 11:32:30 +0000] [1] [INFO] Using worker: sync[2026-10-03 11:32:30 +0000] [7] [INFO] Booting worker with pid: 7# app.py را عوض میکنیم و فقط app دوباره ساخته میشودImage lxsvc-app BuiltContainer lxsvc-redis-1 RunningContainer lxsvc-web-1 RunningContainer lxsvc-app-1 RecreateContainer lxsvc-app-1 RecreatedContainer lxsvc-app-1 Startedlogs --tail 3 app سه خط آخر لاگ سرویس app را میدهد (-f برای دنبال کردن زنده). بعد از تغییر کد، up -d --build فقط سرویس app را بازسازی و دوبارهساخت (Recreated)؛ web و redis که تغییری نکرده بودند دست نخوردند (Running). این همان idempotent بودن Compose است.
پشت پرده
Section titled “پشت پرده”وقتی docker compose up میزنی، Compose ابتدا گراف وابستگی را از روی شبکهها، volume ها و depends_on میسازد، بعد منابع را به ترتیب میسازد: شبکهها و volume ها، سپس سرویسها. برای هر سرویس «حالت مطلوب» (طبق فایل) را با «حالت فعلی» (کانتینر موجود) مقایسه میکند: یک هش از پیکربندی سرویس روی کانتینر ثبت میشود (label)، و اگر هش عوض نشده باشد کانتینر دست نمیخورد. به همین دلیل بعد از تغییر یک سرویس، فقط همان دوباره ساخته میشود.
دو نکته:
- بخشهای top-level:
networks:وvolumes:باید تعریف شوند و در سرویسها فقط ارجاع داده میشوند. نام واقعی روی داکرپروژه_ناماست (lxsvc_redis-data). - اگر شبکه تعریف نکنی، Compose یک شبکهی
پروژه_defaultمیسازد و همهی سرویسها را به آن میبرد؛ فقط وقتی ایزولاسیون میخواهی شبکهی سفارشی بنویس.
جدولهای مرجع
Section titled “جدولهای مرجع”| کلید سرویس | معادل docker run / معنی |
|---|---|
image: |
image آماده |
build: |
ساخت از Dockerfile (context، dockerfile، args) |
ports: |
-p |
volumes: |
-v (bind یا named) |
environment: |
-e |
networks: |
--network |
restart: |
--restart |
command: / entrypoint: |
override دستور اجرا |
depends_on: |
ترتیب شروع (درس بعد) |
| restart policy | معنی |
|---|---|
no (پیشفرض) |
هرگز دوباره نیاید |
on-failure |
فقط اگر با خطا تمام شد |
always |
همیشه (حتی بعد از توقف دستی و ریستارت داکر) |
unless-stopped |
همیشه، مگر دستی متوقف شده باشد |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) شبکه یا volume تعریفنشده
Section titled “۱) شبکه یا volume تعریفنشده”mkdir -p m1 && printf 'services:\n a:\n image: alpine\n networks:\n - ghost\n' > m1/compose.yamlcd m1 && docker compose config 2>&1 | head -1service "a" refers to undefined network ghost: invalid compose projectسرویس به شبکهای ارجاع داده که در بخش top-level نیست. راهحل: در networks: بالا تعریفش کن.
۲) مسیر build اشتباه
Section titled “۲) مسیر build اشتباه”mkdir -p m2 && printf 'services:\n a:\n build: ./nodir\n' > m2/compose.yamlcd m2 && docker compose build 2>&1 | grep 'unable to prepare' | sed -E 's#"[^"]*/nodir"#"…/nodir"#'unable to prepare context: path "…/nodir" not foundپوشهی context وجود ندارد. راهحل: مسیر نسبت به محل فایل Compose است؛ آن را درست بنویس.
۳) down -v و از دست رفتن داده
Section titled “۳) down -v و از دست رفتن داده”مثال ۲: همهی دادهی volume رفت. راهحل: فقط وقتی از اول میخواهی -v بزن؛ و برای دادهی مهم بکاپ بگیر (درس volume).
۴) bind mount با مسیر نسبی بدون ./
Section titled “۴) bind mount با مسیر نسبی بدون ./”mkdir -p m4 && printf 'services:\n a:\n image: alpine\n volumes:\n - data:/x\nvolumes: {}\n' > m4/compose.yamlcd m4 && docker compose config 2>&1 | head -1 | cut -c1-120service "a" refers to undefined volume data: invalid compose projectdata:/x یعنی یک named volume به اسم data (که تعریف نشده → خطا). برای پوشهی میزبان بنویس ./data:/x. راهحل: همیشه ./ برای bind mount.
۵) فراموش کردن --build
Section titled “۵) فراموش کردن --build”بعد از تغییر کد اپ، اگر فقط up -d بزنی image قدیمی استفاده میشود. راهحل: up -d --build.
در پروژهی shop نشان بده volume lxsvc_redis-data وجود دارد و به چه سرویسی وصل است (با docker volume ls و docker compose config --volumes).
دیدن جواب
docker volume ls --filter name=lxsvc --format '{{.Name}}'cd shop && docker compose config --volumeslxsvc_redis-dataredis-dataتمرین اصلی: یک پروژهی سهسرویسی بنویس: web (nginx)، app (alpine با sleep) و cache (redis:alpine)، با دو شبکهی front و back بهطوری که web فقط در front، cache فقط در back و app در هر دو باشد. با docker compose config ثابت کن هر سرویس به چه شبکهای وصل است.
دیدن جواب
mkdir -p e2 && cat > e2/compose.yaml <<'LXEOF'name: lxe2services: web: image: nginx:alpine networks: [front] app: image: alpine command: sleep 60 networks: [front, back] cache: image: redis:alpine networks: [back]networks: front: back:LXEOFcd e2 && docker compose config | grep -E "^ (web|app|cache):|^ (front|back):" | awk '/^ [a-z]+:/{s=$1} /^ /{print s, $1}'app: back:app: front:cache: back:web: front:ثابت کن restart: unless-stopped چه فرقی با restart: "no" دارد: دو سرویس بساز که پروسهی اصلیشان با kill 1 میمیرد؛ یکی با unless-stopped و دیگری پیشفرض. بعد از چند ثانیه وضعیت هر دو را بخوان.
دیدن جواب
mkdir -p e3 && cat > e3/compose.yaml <<'LXEOF'name: lxe3services: keeps: image: alpine command: sh -c "sleep 3; exit 1" restart: unless-stopped dies: image: alpine command: sh -c "sleep 3; exit 1"LXEOFcd e3 && docker compose up -d >/dev/null 2>&1; sleep 12docker inspect lxe3-keeps-1 --format 'keeps: status={{.State.Status}} restarts={{.RestartCount}}'docker inspect lxe3-dies-1 --format 'dies: status={{.State.Status}} restarts={{.RestartCount}}'docker compose down >/dev/null 2>&1keeps: status=running restarts=3dies: status=exited restarts=0سرویس با unless-stopped بعد از هر خروج دوباره شروع شد (restarts > 0)؛ سرویس پیشفرض یک بار اجرا شد و exited ماند.
آزمونک
Section titled “آزمونک”کلید build: ./app چه میکند؟
معادل docker build ./app.
تفاوت docker compose down و down -v؟
بدون -v دادهی volume میماند.
چرا سرویس web نمیتواند redis را ببیند؟
فقط سرویسهای همشبکه همدیگر را resolve میکنند.
restart: unless-stopped یعنی…
برای سرویسهای ماندگار مثل دیتابیس.
بعد از تغییر کد اپ چه میزنی که image دوباره ساخته شود؟
بدون --build image قدیمی استفاده میشود.
جمعبندی
Section titled “جمعبندی”- هر سرویس:
imageیاbuild،ports،volumes،environment،networks،restart. - بخشهای top-level
networks:وvolumes:را تعریف کن و در سرویسها ارجاع بده. - شبکههای سفارشی ایزولهسازی میدهند؛ بدون شبکه، همه در
پروژه_defaultهستند. up -d --buildفقط سرویسهای تغییرکرده را دوباره میسازد؛downدادهی volume را نگه میدارد وdown -vپاک میکند.restart: unless-stoppedبرای سرویسهای ماندگار؛docker compose logs -fلاگ زنده.
| دستور | کاری که میکند |
|---|---|
docker compose up -d --build | ساخت image ها و اجرا |
docker compose logs -f SERVICE | لاگ زندهی یک سرویس |
docker compose down | حذف کانتینرها و شبکه (volume میماند) |
docker compose down -v | حذف volume ها هم (داده میرود!) |
docker compose config --volumes | فهرست volume های تعریفشده |
build: ./dir | ساخت از Dockerfile |
volumes: - ./src:/app:ro | bind mount فقطخواندنی |
volumes: - data:/var/lib/x | named volume |
restart: unless-stopped | ریاستارت خودکار |
networks: [front, back] | عضویت در چند شبکه |