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

سرویس‌ها، 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:).

سرویس web فقط روی شبکه‌ی frontend، app روی هر دو و redis فقط روی backend است؛ داده‌ی redis روی یک volume نام‌دار.

مثال ۱: پروژه‌ی سه‌سرویسی

Section titled “مثال ۱: پروژه‌ی سه‌سرویسی”

اول اپ Flask (یک شمارنده در Redis) و صفحه‌ی وب:

shop/app/app.py
import os
from flask import Flask, jsonify
import redis
app = Flask(__name__)
app.json.ensure_ascii = False
r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379)
@app.get("/")
def hits():
return jsonify(hits=r.incr("hits"), message="شمارنده در Redis ذخیره می‌شود")
shop/app/requirements.txt
flask==3.0.3
gunicorn==22.0.0
redis==5.0.8
shop/app/Dockerfile
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
shop/web/index.html
<!doctype html><meta charset="utf-8"><h1>صفحه‌ی استاتیک (nginx)</h1>

و فایل Compose:

shop/compose.yaml
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 داخل پوشه‌ی ./app image می‌سازد (معادل docker build).
  • ports: مثل -p؛ volumes: هم bind mount (./web:/...:ro) و هم named volume (redis-data:/data).
  • environment: متغیر محیطی؛ اینجا به اپ می‌گوید Redis کجاست (اسم سرویس).
  • networks: کدام شبکه‌ها؛ بخش top-level networks: و volumes: آن‌ها را تعریف می‌کند (بدون تعریف، خطا می‌گیری).
  • restart: unless-stopped: اگر کانتینر بیفتد، خودکار دوباره بالا بیاید (مگر خودت متوقفش کرده باشی).
Terminal window
cd shop
docker compose up -d --build 2>&1 | grep -E "Built|Started|Created|Healthy" | awk '!s[$0]++' | sed -E 's/^ +//' | head -8
sleep 3
docker compose ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}' | sed 's/, \[::\][^ ]*//'
خروجی
Image lxsvc-app Built
Volume lxsvc_redis-data Created
Network lxsvc_frontend Created
Network lxsvc_backend Created
Container lxsvc-app-1 Created
Container lxsvc-redis-1 Created
Container lxsvc-web-1 Created
Container lxsvc-app-1 Started
SERVICE STATUS PORTS
app Up 4 seconds 0.0.0.0:8384->8000/tcp
redis Up 4 seconds 6379/tcp
web Up 3 seconds 0.0.0.0:8383->80/tcp

--build یعنی image سرویس‌هایی که build: دارند اول ساخته (یا به‌روز) شوند. بعد همه‌ی سه سرویس بالا آمدند. حالا امتحان:

Terminal window
for i in 1 2 3; do curl -s localhost:8384/; echo; done
curl -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 در سرویس چه می‌کند؟

مثال ۲: داده با down می‌ماند، با down -v می‌رود

Section titled “مثال ۲: داده با down می‌ماند، با down -v می‌رود”
Terminal window
cd shop
docker compose down 2>&1 | grep -cE "Removed"
docker compose up -d >/dev/null 2>&1; sleep 3
echo "بعد از down و up دوباره: $(curl -s localhost:8384/)"
docker compose down -v 2>&1 | grep -E "Volume" | head -1
docker compose up -d >/dev/null 2>&1; sleep 3
echo "بعد از 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 “مثال ۳: ایزوله‌سازی با شبکه‌ها”
Terminal window
cd shop
docker 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.3
web -> redis: resolve نمی‌شود
app روی: lxsvc_backend lxsvc_frontend
web روی: lxsvc_frontend

اپ در هر دو شبکه است و Redis را می‌بیند؛ وب فقط در frontend است و Redis را نمی‌بیند. همین ایزوله‌سازی است که در سناریو خواستیم، و بدون هیچ دستور شبکه‌ای: فقط با دو کلید در فایل.

Terminal window
cd shop
docker inspect lxsvc-redis-1 --format 'قبل: restarts={{.RestartCount}} policy={{.HostConfig.RestartPolicy.Name}}'
docker compose exec -T redis sh -c 'kill 1' 2>&1 | head -1
sleep 6
docker 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 “مثال ۵: لاگ‌ها و بازسازی بعد از تغییر کد”
Terminal window
cd shop
docker compose logs --tail 3 app 2>&1 | sed -E 's/^[^|]+\| //' | cut -c1-90
echo "# app.py را عوض می‌کنیم و فقط app دوباره ساخته می‌شود"
echo "# تغییر" >> app/app.py
docker 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 Built
Container lxsvc-redis-1 Running
Container lxsvc-web-1 Running
Container lxsvc-app-1 Recreate
Container lxsvc-app-1 Recreated
Container lxsvc-app-1 Started

logs --tail 3 app سه خط آخر لاگ سرویس app را می‌دهد (-f برای دنبال کردن زنده). بعد از تغییر کد، up -d --build فقط سرویس app را بازسازی و دوباره‌ساخت (Recreated)؛ web و redis که تغییری نکرده بودند دست نخوردند (Running). این همان idempotent بودن Compose است.

وقتی docker compose up می‌زنی، Compose ابتدا گراف وابستگی را از روی شبکه‌ها، volume ها و depends_on می‌سازد، بعد منابع را به ترتیب می‌سازد: شبکه‌ها و volume ها، سپس سرویس‌ها. برای هر سرویس «حالت مطلوب» (طبق فایل) را با «حالت فعلی» (کانتینر موجود) مقایسه می‌کند: یک هش از پیکربندی سرویس روی کانتینر ثبت می‌شود (label)، و اگر هش عوض نشده باشد کانتینر دست نمی‌خورد. به همین دلیل بعد از تغییر یک سرویس، فقط همان دوباره ساخته می‌شود.

دو نکته:

  • بخش‌های top-level: networks: و volumes: باید تعریف شوند و در سرویس‌ها فقط ارجاع داده می‌شوند. نام واقعی روی داکر پروژه_نام است (lxsvc_redis-data).
  • اگر شبکه تعریف نکنی، Compose یک شبکه‌ی پروژه_default می‌سازد و همه‌ی سرویس‌ها را به آن می‌برد؛ فقط وقتی ایزولاسیون می‌خواهی شبکه‌ی سفارشی بنویس.
کلید سرویس معادل 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 همیشه، مگر دستی متوقف شده باشد

۱) شبکه یا volume تعریف‌نشده

Section titled “۱) شبکه یا volume تعریف‌نشده”
Terminal window
mkdir -p m1 && printf 'services:\n a:\n image: alpine\n networks:\n - ghost\n' > m1/compose.yaml
cd m1 && docker compose config 2>&1 | head -1
خروجی
service "a" refers to undefined network ghost: invalid compose project

سرویس به شبکه‌ای ارجاع داده که در بخش top-level نیست. راه‌حل: در networks: بالا تعریفش کن.

Terminal window
mkdir -p m2 && printf 'services:\n a:\n build: ./nodir\n' > m2/compose.yaml
cd 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 با مسیر نسبی بدون ./”
Terminal window
mkdir -p m4 && printf 'services:\n a:\n image: alpine\n volumes:\n - data:/x\nvolumes: {}\n' > m4/compose.yaml
cd m4 && docker compose config 2>&1 | head -1 | cut -c1-120
خروجی
service "a" refers to undefined volume data: invalid compose project

data:/x یعنی یک named volume به اسم data (که تعریف نشده → خطا). برای پوشه‌ی میزبان بنویس ./data:/x. راه‌حل: همیشه ./ برای bind mount.

بعد از تغییر کد اپ، اگر فقط up -d بزنی image قدیمی استفاده می‌شود. راه‌حل: up -d --build.

✎ تمرینآسان

در پروژه‌ی shop نشان بده volume lxsvc_redis-data وجود دارد و به چه سرویسی وصل است (با docker volume ls و docker compose config --volumes).

دیدن جواب
Terminal window
docker volume ls --filter name=lxsvc --format '{{.Name}}'
cd shop && docker compose config --volumes
خروجی
lxsvc_redis-data
redis-data
✎ تمرینمتوسط

تمرین اصلی: یک پروژه‌ی سه‌سرویسی بنویس: web (nginx)، app (alpine با sleep) و cache (redis:alpine)، با دو شبکه‌ی front و back به‌طوری که web فقط در front، cache فقط در back و app در هر دو باشد. با docker compose config ثابت کن هر سرویس به چه شبکه‌ای وصل است.

دیدن جواب
Terminal window
mkdir -p e2 && cat > e2/compose.yaml <<'LXEOF'
name: lxe2
services:
web:
image: nginx:alpine
networks: [front]
app:
image: alpine
command: sleep 60
networks: [front, back]
cache:
image: redis:alpine
networks: [back]
networks:
front:
back:
LXEOF
cd 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 و دیگری پیش‌فرض. بعد از چند ثانیه وضعیت هر دو را بخوان.

دیدن جواب
Terminal window
mkdir -p e3 && cat > e3/compose.yaml <<'LXEOF'
name: lxe3
services:
keeps:
image: alpine
command: sh -c "sleep 3; exit 1"
restart: unless-stopped
dies:
image: alpine
command: sh -c "sleep 3; exit 1"
LXEOF
cd e3 && docker compose up -d >/dev/null 2>&1; sleep 12
docker 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>&1
خروجی
keeps: status=running restarts=3
dies: status=exited restarts=0

سرویس با unless-stopped بعد از هر خروج دوباره شروع شد (restarts > 0)؛ سرویس پیش‌فرض یک بار اجرا شد و exited ماند.

؟ آزمونک
  1. کلید build: ./app چه می‌کند؟

  2. تفاوت docker compose down و down -v؟

  3. چرا سرویس web نمی‌تواند redis را ببیند؟

  4. restart: unless-stopped یعنی…

  5. بعد از تغییر کد اپ چه می‌زنی که image دوباره ساخته شود؟

  • هر سرویس: 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:robind mount فقط‌خواندنی
volumes: - data:/var/lib/xnamed volume
restart: unless-stoppedری‌استارت خودکار
networks: [front, back]عضویت در چند شبکه