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

دستورات کاربردی Compose

توی این درس یاد می‌گیری دستورهایی را که هر روز با Compose می‌زنی بشناسی: docker compose build و docker compose pull برای آماده‌سازی image ها، up / stop / restart / down برای چرخه‌ی عمر پروژه، و مهم‌تر از همه فرق docker compose exec (داخل کانتینرِ در حال اجرا) با docker compose run --rm (یک کانتینر تازه و یک‌بارمصرف). با run --rm یک migration دیتابیس را اجرا می‌کنی و با docker compose up --scale app=3 چند نمونه از یک سرویس بالا می‌آوری.

تشبیه: ریموت کنترل پروژه

Section titled “تشبیه: ریموت کنترل پروژه”

فایل Compose مثل نقشه‌ی یک کارگاه است و دستورهای docker compose ریموت کنترل آن: همه‌ی ماشین‌ها را با یک دکمه روشن می‌کنی (up)، یکی را خاموش و روشن می‌کنی (restart)، داخل یکی سر می‌زنی (exec)، یا یک ماشین موقت می‌آوری برای یک کار یک‌باره (run).

exec داخل کانتینر موجود و در حال اجرا وارد می‌شود. run یک کانتینر تازه از همان سرویس می‌سازد، دستور را اجرا می‌کند و (با --rm) پاک می‌شود.

پروژه‌ی این درس دو سرویس دارد: web (nginx) و worker (یک image پایتونی که از Dockerfile ساخته می‌شود، یک volume برای داده دارد و یک اسکریپت migration دارد).

cmds/worker/migrate.py
import sqlite3
con = sqlite3.connect("/data/app.db")
con.execute("create table if not exists schema_version(v integer)")
current = con.execute("select max(v) from schema_version").fetchone()[0] or 0
migrations = [
"create table users(id integer primary key, name text)",
"insert into users(name) values ('ali'), ('sara')",
"alter table users add column email text",
]
for i, sql in enumerate(migrations, 1):
if i > current:
con.execute(sql)
con.execute("insert into schema_version values (?)", (i,))
print("applied migration", i)
con.commit()
print("schema version:", max(current, len(migrations)))
cmds/worker/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY migrate.py .
CMD ["python", "-c", "import time; time.sleep(3600)"]
cmds/compose.yaml
name: lxcmd
services:
web:
image: nginx:alpine
ports:
- "8390-8392:80"
worker:
build: ./worker
volumes:
- data:/data
volumes:
data:

pull image های آماده (image:) را می‌گیرد و build سرویس‌های دارای build: را می‌سازد:

Terminal window
cd cmds
docker compose pull web 2>&1 | grep -E "Pulled" | sed -E 's/^ +//' | head -1
docker compose build 2>&1 | grep -E "Built|naming" | sed -E 's/^ +//' | head -2
docker image ls lxcmd-worker --format 'image ساخته‌شده: {{.Repository}}:{{.Tag}}'
خروجی
Image nginx:alpine Pulled
#9 naming to docker.io/library/lxcmd-worker:latest 0.0s done
Image lxcmd-worker Built
image ساخته‌شده: lxcmd-worker:latest

pull web تنها سرویس web را به‌روز می‌کند (بدون نام سرویس، همه‌ی image: ها). build فقط سرویس‌های دارای build: را می‌سازد (اینجا worker) و از کش استفاده می‌کند. pull --ignore-buildable اگر بخواهی سرویس‌های ساختنی را رد کند.

مثال ۲: چرخه‌ی عمر: up، ps، logs، stop، start، restart

Section titled “مثال ۲: چرخه‌ی عمر: up، ps، logs، stop، start، restart”
Terminal window
cd cmds
docker compose up -d 2>&1 | grep -E "Started" | sed -E 's/^ +//' | sort
docker compose ps --format '{{.Service}}: {{.State}}' | sort
echo "# توقف و شروع دوباره (کانتینر حذف نمی‌شود)"
docker compose stop worker 2>&1 | grep -E "Stopped" | sed -E 's/^ +//'
docker compose ps -a --format '{{.Service}}: {{.State}}' | sort
docker compose start worker 2>&1 | grep -E "Started" | sed -E 's/^ +//'
docker compose restart worker 2>&1 | grep -E "Started" | sed -E 's/^ +//'
خروجی
Container lxcmd-web-1 Started
Container lxcmd-worker-1 Started
web: running
worker: running
# توقف و شروع دوباره (کانتینر حذف نمی‌شود)
Container lxcmd-worker-1 Stopped
web: running
worker: exited
Container lxcmd-worker-1 Started
Container lxcmd-worker-1 Started
  • stop کانتینر را متوقف می‌کند ولی نگه می‌دارد (در ps -a دیده می‌شود)، start دوباره روشنش می‌کند.
  • restart معادل stop و start است.
  • down برخلاف stop کانتینرها و شبکه را حذف می‌کند.
⚡ بررسی سریع

فرق docker compose stop با down چیست؟

مثال ۳: exec در برابر run --rm

Section titled “مثال ۳: exec در برابر run --rm”
Terminal window
cd cmds
a=$(docker compose exec -T worker hostname)
b=$(docker compose run --rm -T worker hostname 2>/dev/null | tail -1)
c=$(docker compose exec -T worker hostname)
[ "$a" = "$c" ] && echo "exec دو بار: همان کانتینر"
[ "$a" != "$b" ] && echo "run --rm: یک کانتینر تازه و متفاوت"
echo "کانتینرهای worker بعد از run --rm: $(docker compose ps -a --format '{{.Service}}' | grep -c worker)"
خروجی
exec دو بار: همان کانتینر
run --rm: یک کانتینر تازه و متفاوت
کانتینرهای worker بعد از run --rm: 1

exec هر دو بار داخل همان کانتینر رفت (hostname یکسان). run --rm یک کانتینر جدید ساخت (hostname متفاوت)، دستور را اجرا کرد و بعد پاکش کرد؛ در پایان فقط همان یک کانتینر worker اصلی مانده. پرچم -T برای اجرای غیرتعاملی در اسکریپت است (بدون ترمینال). در ترمینال معمولی لازم نیست.

برای شل تعاملی داخل کانتینر در حال اجرا: docker compose exec worker sh.

مثال ۴: اجرای migration با run --rm

Section titled “مثال ۴: اجرای migration با run --rm”
Terminal window
cd cmds
echo "# بار اول:"
docker compose run --rm worker python migrate.py 2>&1 | grep -vE "Container|Network|Volume"
echo "# بار دوم (idempotent):"
docker compose run --rm worker python migrate.py 2>&1 | grep -vE "Container|Network|Volume"
echo "# بررسی داده‌ها:"
docker compose run --rm worker python -c "import sqlite3; print(sqlite3.connect('/data/app.db').execute('select id, name, email from users').fetchall())" 2>&1 | grep -vE "Container|Network|Volume"
خروجی
# بار اول:
applied migration 1
applied migration 2
applied migration 3
schema version: 3
# بار دوم (idempotent):
schema version: 3
# بررسی داده‌ها:
[(1, 'ali', None), (2, 'sara', None)]

اسکریپت سه migration دارد. بار اول هر سه اعمال شد؛ بار دوم هیچ‌کدام (چون نسخه‌ی schema ثبت شده است). داده در volume data ماند و بین اجراهای run --rm مشترک بود، پس کانتینر موقت به همان دیتابیس رسید. همین الگو را برای migration واقعی (Django، Alembic، Prisma…) به کار می‌بری: docker compose run --rm app <دستور migration>.

مثال ۵: scale، چند نمونه از یک سرویس

Section titled “مثال ۵: scale، چند نمونه از یک سرویس”
Terminal window
cd cmds
docker compose up -d --scale web=3 2>&1 | grep -E "Started|Running" | sed -E 's/^ +//' | sort
docker compose ps --format '{{.Name}}' | grep web | sort
for p in 8390 8391 8392; do echo "پورت $p: HTTP $(curl -s -o /dev/null -w '%{http_code}' localhost:$p)"; done
خروجی
Container lxcmd-web-1 Running
Container lxcmd-web-2 Started
Container lxcmd-web-3 Started
Container lxcmd-worker-1 Running
lxcmd-web-1
lxcmd-web-2
lxcmd-web-3
پورت 8390: HTTP 200
پورت 8391: HTTP 200
پورت 8392: HTTP 200

سه کانتینر web ساخته شد (lxcmd-web-1 تا -3). چون در فایل ports: "8390-8392:80" را بازه نوشتیم، هر نمونه یک پورت از بازه گرفت. اگر ports: "8390:80" بود، نمونه‌ی دوم به‌خاطر درگیری پورت بالا نمی‌آمد؛ برای scale یا پورت را بازه کن، یا (بهتر) هیچ پورت میزبانی نده و جلوی همه یک reverse proxy بگذار.

همه‌ی نمونه‌ها زیر یک اسم DNS (web) هستند:

Terminal window
cd cmds
docker compose exec -T worker python -c "import socket; print(len(sorted({a[4][0] for a in socket.getaddrinfo('web', 80)})), 'آدرس برای web')"
خروجی
3 آدرس برای web

در داخل شبکه‌ی Compose، اسم web به هر سه کانتینر ترجمه می‌شود (round-robin DNS، همان که در درس DNS دیدی). برگرداندن به یک نمونه: docker compose up -d --scale web=1.

مثال ۶: لاگ و دیدن وضعیت پروژه

Section titled “مثال ۶: لاگ و دیدن وضعیت پروژه”
Terminal window
cd cmds
docker compose logs --no-log-prefix --tail 1 web 2>&1 | head -1 | cut -c1-80
docker compose ls --format json | python3 -c "import json,sys; print([(p['Name'], p['Status']) for p in json.load(sys.stdin) if p['Name']=='lxcmd'])"
docker compose config --services | sort
خروجی
192.168.65.1 - - [03/Oct/2026:11:45:34 +0000] "GET / HTTP/1.1" 200 896 "-" "curl
[('lxcmd', 'running(4)')]
web
worker

logs --tail N آخرین N خط، ls پروژه‌های Compose روی این ماشین، و config --services فهرست سرویس‌ها. بعد از کار:

Terminal window
cd cmds
docker compose down -v 2>&1 | grep -E "Removed" | sed -E 's/^ +//' | sort -u | head -8
docker compose ps -a | wc -l | tr -d ' '
خروجی
Container lxcmd-web-1 Removed
Container lxcmd-web-2 Removed
Container lxcmd-web-3 Removed
Container lxcmd-worker-1 Removed
Network lxcmd_default Removed
Volume lxcmd_data Removed
1

down -v همه‌ی کانتینرها، شبکه و volume را برچید؛ فقط خط هدر ps مانده (۱).

همه‌ی دستورهای docker compose در عمل همان API داکر (همان که docker استفاده می‌کند) را صدا می‌زنند؛ Compose یک برچسب پروژه (com.docker.compose.project) و برچسب سرویس و شماره‌ی نمونه روی هر کانتینر می‌گذارد. با همین برچسب‌هاست که docker compose ps می‌داند کانتینر مال کدام پروژه است، و scale می‌داند شماره‌ی بعدی چند است.

  • exec = docker exec روی کانتینر انتخاب‌شده‌ی سرویس.
  • run = ساختن یک کانتینر جدید با پیکربندی همان سرویس (image، env، volume، شبکه)، با دستور تو؛ پورت‌های میزبان را باز نمی‌کند مگر --service-ports بدهی (تا با سرویس اصلی تداخل نکند).
  • run وابستگی‌های depends_on را هم بالا می‌آورد (مگر --no-deps).
دستور کار
docker compose build [svc] ساخت image سرویس‌های دارای build:
docker compose pull [svc] گرفتن image های image:
docker compose up -d ساخت (اگر لازم باشد) و اجرا در پس‌زمینه
docker compose stop / start توقف/شروع (بدون حذف)
docker compose restart [svc] راه‌اندازی دوباره
docker compose down [-v] برچیدن؛ -v volume ها هم
docker compose ps [-a] وضعیت کانتینرهای پروژه
docker compose logs [-f] [svc] لاگ
docker compose exec svc cmd دستور داخل کانتینر در حال اجرا
docker compose run --rm svc cmd کانتینر یک‌بارمصرف
docker compose up --scale svc=N N نمونه
flag مهم معنی
up --build قبل از اجرا دوباره بساز
up --force-recreate کانتینرها را حتی بدون تغییر دوباره بساز
up --no-deps svc فقط همان سرویس، بدون وابستگی‌ها
run --no-deps بدون بالا آوردن وابستگی‌ها
run --service-ports پورت‌های سرویس را هم باز کن
exec -T / run -T بدون ترمینال (برای اسکریپت و CI)
down --remove-orphans کانتینرهای سرویس‌های حذف‌شده از فایل را هم پاک کن

۱) exec روی سرویسی که بالا نیست

Section titled “۱) exec روی سرویسی که بالا نیست”
Terminal window
mkdir -p m1 && printf 'services:\n a:\n image: alpine\n command: sleep 30\n' > m1/compose.yaml
cd m1 && docker compose exec a ls 2>&1 | head -1
خروجی
service "a" is not running

سرویس هنوز اجرا نشده. راه‌حل: اول up -d، یا برای کار یک‌بار از run --rm استفاده کن.

Terminal window
cd m1 && docker compose run --rm nope ls 2>&1 | head -1
خروجی
no such service: nope

به‌جای نام کانتینر، نام سرویس (کلید زیر services:) را بنویس. با docker compose config --services فهرستش را ببین.

بدون --rm هر run یک کانتینر متوقف‌شده به جا می‌گذارد:

Terminal window
cd m1
docker compose run -T a true >/dev/null 2>&1
docker compose run -T a true >/dev/null 2>&1
echo "کانتینرهای باقی‌مانده: $(docker compose ps -a --format '{{.Name}}' | grep -c run)"
docker compose down --remove-orphans >/dev/null 2>&1
خروجی
کانتینرهای باقی‌مانده: 2

دو بار run دو کانتینر ...-run-... به جا گذاشت. راه‌حل: همیشه run --rm.

Terminal window
mkdir -p m4 && printf 'services:\n w:\n image: nginx:alpine\n ports:\n - "8393:80"\n' > m4/compose.yaml
cd m4 && docker compose up -d --scale w=2 2>&1 | grep -oE 'Bind for [^ ]+ failed: port is already allocated' | head -1
docker compose down >/dev/null 2>&1
خروجی
Bind for 0.0.0.0:8393 failed: port is already allocated

دو نمونه نمی‌توانند یک پورت میزبان را بگیرند. راه‌حل: بازه‌ی پورت بده یا پورت میزبان نده و از reverse proxy استفاده کن.

اگر برای سرویس container_name: x بگذاری، دیگر نمی‌توانی scale کنی (اسم‌ها یکتا هستند). راه‌حل: container_name ننویس و بگذار Compose اسم بدهد.

✎ تمرینآسان

در پروژه‌ی cmds (دوباره بالا بیاور) با exec نشان بده نسخه‌ی پایتون داخل worker چند است، و بعد با run --rm همان را از یک کانتینر تازه بخوان.

دیدن جواب
Terminal window
cd cmds
docker compose up -d worker >/dev/null 2>&1
docker compose exec -T worker python --version
docker compose run --rm -T worker python --version 2>/dev/null | tail -1
docker compose down -v >/dev/null 2>&1
خروجی
Python 3.12.15
Python 3.12.15
✎ تمرینمتوسط

تمرین اصلی: یک migration دیتابیس را با compose run اجرا کن. در یک پروژه‌ی تازه، اسکریپت پایتونی بنویس که جدول items بسازد و سه ردیف اضافه کند؛ آن را با docker compose run --rm اجرا کن و بعد با یک run --rm دیگر تعداد ردیف‌ها را بخوان (داده باید در volume بماند).

دیدن جواب
Terminal window
mkdir -p e2 && cd e2
cat > compose.yaml <<'LXEOF'
name: lxe2
services:
tool:
image: python:3.12-slim
working_dir: /w
volumes:
- ./:/w
- db:/data
volumes:
db:
LXEOF
cat > mig.py <<'LXEOF'
import sqlite3
c = sqlite3.connect("/data/x.db")
c.execute("create table if not exists items(id integer primary key, n text)")
c.executemany("insert into items(n) values (?)", [("a",), ("b",), ("c",)])
c.commit()
print("migrated")
LXEOF
docker compose run --rm tool python mig.py 2>&1 | grep -vE "Container|Network|Volume"
docker compose run --rm tool python -c "import sqlite3; print('rows:', sqlite3.connect('/data/x.db').execute('select count(*) from items').fetchone()[0])" 2>&1 | grep -vE "Container|Network|Volume"
docker compose down -v >/dev/null 2>&1
خروجی
migrated
rows: 3
✎ تمرینسخت

با up --scale سه نمونه از یک سرویس nginx بدون هیچ پورت میزبان بالا بیاور و از داخل یک سرویس دیگر (alpine با nslookup یا getent) نشان بده اسم سرویس به سه IP ترجمه می‌شود؛ بعد scale را به ۱ برگردان و نشان بده فقط یک IP می‌ماند.

دیدن جواب
Terminal window
mkdir -p e3 && cd e3
cat > compose.yaml <<'LXEOF'
name: lxe3
services:
web:
image: nginx:alpine
probe:
image: alpine
command: sleep 120
LXEOF
docker compose up -d --scale web=3 >/dev/null 2>&1
sleep 1
echo "با scale=3: $(docker compose exec -T probe getent ahostsv4 web | awk '{print $1}' | sort -u | wc -l | tr -d ' ') IP"
docker compose up -d --scale web=1 >/dev/null 2>&1
sleep 1
echo "با scale=1: $(docker compose exec -T probe getent ahostsv4 web | awk '{print $1}' | sort -u | wc -l | tr -d ' ') IP"
docker compose down -v >/dev/null 2>&1
خروجی
با scale=3: 3 IP
با scale=1: 1 IP
؟ آزمونک
  1. تفاوت اصلی exec و run --rm؟

  2. برای اجرای یک migration تک‌بار چه می‌زنی؟

  3. چرا up --scale web=3 با ports: "8390:80" خطا می‌دهد؟

  4. docker compose stop و down چه فرقی دارند؟

  5. پرچم -T برای چیست؟

  6. docker compose pull چه می‌کند؟

  • آماده‌سازی: pull برای image:، build برای build:؛ up --build هر دو کار را در یک دستور انجام می‌دهد.
  • چرخه‌ی عمر: up -d، stop/start/restart، و down [-v] برای برچیدن.
  • exec داخل کانتینر در حال اجرا (دیباگ)؛ run --rm کانتینر یک‌بارمصرف با همان تنظیمات (migration، تست).
  • up --scale svc=N چند نمونه می‌سازد؛ پورت میزبان ثابت و container_name با scale جور نیست؛ اسم سرویس DNS چندگانه است.
  • در اسکریپت و CI از -T استفاده کن و همیشه --rm با run.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker compose pull [svc]گرفتن image های آماده
docker compose build [svc]ساخت image ها
docker compose up -d --buildساخت و اجرا
docker compose stop / start / restart [svc]مکث و ادامه
docker compose down [-v]برچیدن پروژه
docker compose exec svc shشل داخل کانتینر در حال اجرا
docker compose run --rm svc cmdکانتینر یک‌بارمصرف (migration)
docker compose up -d --scale web=3سه نمونه از web
docker compose ps -aهمه‌ی کانتینرهای پروژه
docker compose config --servicesفهرست سرویس‌ها