توی این درس یاد میگیری دستورهایی را که هر روز با 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
Section titled “exec در برابر run”مثالهای عملی
Section titled “مثالهای عملی”پروژهی این درس دو سرویس دارد: web (nginx) و worker (یک image پایتونی که از Dockerfile ساخته میشود، یک volume برای داده دارد و یک اسکریپت migration دارد).
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 0migrations = [ "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)))FROM python:3.12-slimWORKDIR /appCOPY migrate.py .CMD ["python", "-c", "import time; time.sleep(3600)"]name: lxcmd
services: web: image: nginx:alpine ports: - "8390-8392:80" worker: build: ./worker volumes: - data:/data
volumes: data:مثال ۱: pull و build
Section titled “مثال ۱: pull و build”pull image های آماده (image:) را میگیرد و build سرویسهای دارای build: را میسازد:
cd cmdsdocker compose pull web 2>&1 | grep -E "Pulled" | sed -E 's/^ +//' | head -1docker compose build 2>&1 | grep -E "Built|naming" | sed -E 's/^ +//' | head -2docker image ls lxcmd-worker --format 'image ساختهشده: {{.Repository}}:{{.Tag}}'Image nginx:alpine Pulled#9 naming to docker.io/library/lxcmd-worker:latest 0.0s doneImage lxcmd-worker Builtimage ساختهشده: lxcmd-worker:latestpull web تنها سرویس web را بهروز میکند (بدون نام سرویس، همهی image: ها). build فقط سرویسهای دارای build: را میسازد (اینجا worker) و از کش استفاده میکند. pull --ignore-buildable اگر بخواهی سرویسهای ساختنی را رد کند.
مثال ۲: چرخهی عمر: up، ps، logs، stop، start، restart
Section titled “مثال ۲: چرخهی عمر: up، ps، logs، stop، start، restart”cd cmdsdocker compose up -d 2>&1 | grep -E "Started" | sed -E 's/^ +//' | sortdocker compose ps --format '{{.Service}}: {{.State}}' | sortecho "# توقف و شروع دوباره (کانتینر حذف نمیشود)"docker compose stop worker 2>&1 | grep -E "Stopped" | sed -E 's/^ +//'docker compose ps -a --format '{{.Service}}: {{.State}}' | sortdocker 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 StartedContainer lxcmd-worker-1 Startedweb: runningworker: running# توقف و شروع دوباره (کانتینر حذف نمیشود)Container lxcmd-worker-1 Stoppedweb: runningworker: exitedContainer lxcmd-worker-1 StartedContainer lxcmd-worker-1 Startedstopکانتینر را متوقف میکند ولی نگه میدارد (درps -aدیده میشود)،startدوباره روشنش میکند.restartمعادل stop و start است.downبرخلافstopکانتینرها و شبکه را حذف میکند.
فرق docker compose stop با down چیست؟
stop/start برای مکث؛ down برای برچیدن پروژه.
مثال ۳: exec در برابر run --rm
Section titled “مثال ۳: exec در برابر run --rm”cd cmdsa=$(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: 1exec هر دو بار داخل همان کانتینر رفت (hostname یکسان). run --rm یک کانتینر جدید ساخت (hostname متفاوت)، دستور را اجرا کرد و بعد پاکش کرد؛ در پایان فقط همان یک کانتینر worker اصلی مانده. پرچم -T برای اجرای غیرتعاملی در اسکریپت است (بدون ترمینال). در ترمینال معمولی لازم نیست.
برای شل تعاملی داخل کانتینر در حال اجرا: docker compose exec worker sh.
مثال ۴: اجرای migration با run --rm
Section titled “مثال ۴: اجرای migration با run --rm”cd cmdsecho "# بار اول:"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 1applied migration 2applied migration 3schema 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، چند نمونه از یک سرویس”cd cmdsdocker compose up -d --scale web=3 2>&1 | grep -E "Started|Running" | sed -E 's/^ +//' | sortdocker compose ps --format '{{.Name}}' | grep web | sortfor p in 8390 8391 8392; do echo "پورت $p: HTTP $(curl -s -o /dev/null -w '%{http_code}' localhost:$p)"; doneContainer lxcmd-web-1 RunningContainer lxcmd-web-2 StartedContainer lxcmd-web-3 StartedContainer lxcmd-worker-1 Runninglxcmd-web-1lxcmd-web-2lxcmd-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) هستند:
cd cmdsdocker 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 “مثال ۶: لاگ و دیدن وضعیت پروژه”cd cmdsdocker compose logs --no-log-prefix --tail 1 web 2>&1 | head -1 | cut -c1-80docker 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 | sort192.168.65.1 - - [03/Oct/2026:11:45:34 +0000] "GET / HTTP/1.1" 200 896 "-" "curl[('lxcmd', 'running(4)')]webworkerlogs --tail N آخرین N خط، ls پروژههای Compose روی این ماشین، و config --services فهرست سرویسها. بعد از کار:
cd cmdsdocker compose down -v 2>&1 | grep -E "Removed" | sed -E 's/^ +//' | sort -u | head -8docker compose ps -a | wc -l | tr -d ' 'Container lxcmd-web-1 RemovedContainer lxcmd-web-2 RemovedContainer lxcmd-web-3 RemovedContainer lxcmd-worker-1 RemovedNetwork lxcmd_default RemovedVolume lxcmd_data Removed1down -v همهی کانتینرها، شبکه و volume را برچید؛ فقط خط هدر ps مانده (۱).
پشت پرده
Section titled “پشت پرده”همهی دستورهای 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).
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
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 |
کانتینرهای سرویسهای حذفشده از فایل را هم پاک کن |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) exec روی سرویسی که بالا نیست
Section titled “۱) exec روی سرویسی که بالا نیست”mkdir -p m1 && printf 'services:\n a:\n image: alpine\n command: sleep 30\n' > m1/compose.yamlcd m1 && docker compose exec a ls 2>&1 | head -1service "a" is not runningسرویس هنوز اجرا نشده. راهحل: اول up -d، یا برای کار یکبار از run --rm استفاده کن.
۲) نام سرویس اشتباه
Section titled “۲) نام سرویس اشتباه”cd m1 && docker compose run --rm nope ls 2>&1 | head -1no such service: nopeبهجای نام کانتینر، نام سرویس (کلید زیر services:) را بنویس. با docker compose config --services فهرستش را ببین.
۳) فراموش کردن --rm
Section titled “۳) فراموش کردن --rm”بدون --rm هر run یک کانتینر متوقفشده به جا میگذارد:
cd m1docker compose run -T a true >/dev/null 2>&1docker compose run -T a true >/dev/null 2>&1echo "کانتینرهای باقیمانده: $(docker compose ps -a --format '{{.Name}}' | grep -c run)"docker compose down --remove-orphans >/dev/null 2>&1کانتینرهای باقیمانده: 2دو بار run دو کانتینر ...-run-... به جا گذاشت. راهحل: همیشه run --rm.
۴) scale با پورت ثابت
Section titled “۴) scale با پورت ثابت”mkdir -p m4 && printf 'services:\n w:\n image: nginx:alpine\n ports:\n - "8393:80"\n' > m4/compose.yamlcd m4 && docker compose up -d --scale w=2 2>&1 | grep -oE 'Bind for [^ ]+ failed: port is already allocated' | head -1docker compose down >/dev/null 2>&1Bind for 0.0.0.0:8393 failed: port is already allocatedدو نمونه نمیتوانند یک پورت میزبان را بگیرند. راهحل: بازهی پورت بده یا پورت میزبان نده و از reverse proxy استفاده کن.
۵) container_name ثابت و scale
Section titled “۵) container_name ثابت و scale”اگر برای سرویس container_name: x بگذاری، دیگر نمیتوانی scale کنی (اسمها یکتا هستند). راهحل: container_name ننویس و بگذار Compose اسم بدهد.
در پروژهی cmds (دوباره بالا بیاور) با exec نشان بده نسخهی پایتون داخل worker چند است، و بعد با run --rm همان را از یک کانتینر تازه بخوان.
دیدن جواب
cd cmdsdocker compose up -d worker >/dev/null 2>&1docker compose exec -T worker python --versiondocker compose run --rm -T worker python --version 2>/dev/null | tail -1docker compose down -v >/dev/null 2>&1Python 3.12.15Python 3.12.15تمرین اصلی: یک migration دیتابیس را با compose run اجرا کن. در یک پروژهی تازه، اسکریپت پایتونی بنویس که جدول items بسازد و سه ردیف اضافه کند؛ آن را با docker compose run --rm اجرا کن و بعد با یک run --rm دیگر تعداد ردیفها را بخوان (داده باید در volume بماند).
دیدن جواب
mkdir -p e2 && cd e2cat > compose.yaml <<'LXEOF'name: lxe2services: tool: image: python:3.12-slim working_dir: /w volumes: - ./:/w - db:/datavolumes: db:LXEOFcat > mig.py <<'LXEOF'import sqlite3c = 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")LXEOFdocker 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>&1migratedrows: 3با up --scale سه نمونه از یک سرویس nginx بدون هیچ پورت میزبان بالا بیاور و از داخل یک سرویس دیگر (alpine با nslookup یا getent) نشان بده اسم سرویس به سه IP ترجمه میشود؛ بعد scale را به ۱ برگردان و نشان بده فقط یک IP میماند.
دیدن جواب
mkdir -p e3 && cd e3cat > compose.yaml <<'LXEOF'name: lxe3services: web: image: nginx:alpine probe: image: alpine command: sleep 120LXEOFdocker compose up -d --scale web=3 >/dev/null 2>&1sleep 1echo "با 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>&1sleep 1echo "با 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آزمونک
Section titled “آزمونک”تفاوت اصلی exec و run --rm؟
migration و کارهای یکبار با run --rm.
برای اجرای یک migration تکبار چه میزنی؟
با همان image و تنظیمات اپ، بدون دست زدن به نمونههای در حال اجرا.
چرا up --scale web=3 با ports: "8390:80" خطا میدهد؟
بازهی پورت بده یا پورت میزبان نده.
docker compose stop و down چه فرقی دارند؟
stop/start مکثاند؛ down برچیدن.
پرچم -T برای چیست؟
exec -T و run -T در اسکریپتها.
docker compose pull چه میکند؟
سرویسهای دارای build: را با build بساز.
جمعبندی
Section titled “جمعبندی”- آمادهسازی:
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 | فهرست سرویسها |