Docker Compose ابزاری است که با آن چند کانتینر مرتبط را با یک فایل تعریف و با یک دستور اجرا میکنید. بهجای اینکه برای برنامه، پایگاهداده و کش سه دستور طولانی docker run با دهها پارامتر بنویسید و یادتان بماند کدام شبکه و کدام volume به کدام وصل است، همه را در فایلی به اسم compose.yaml مینویسید و با docker compose up -d کل مجموعه را بالا میآورید. کامپوز خودش شبکهی مشترک میسازد، volumeها را ایجاد میکند و سرویسها را به ترتیب درست اجرا میکند.
در این نوشته یک مثال واقعی میسازیم: یک برنامهی وب کوچک همراه PostgreSQL و Redis. در همین مثال میبینیم volumeهای نامدار چطور داده را نگه میدارند، healthcheck چیست و چرا depends_on بدون آن کافی نیست، و دستورهای روزمره کداماند، از جمله دستوری که میتواند کل پایگاهدادهتان را پاک کند. اگر هنوز داکر نصب نکردهاید، اول نصب داکر روی اوبونتو ۲۴.۰۴ را ببینید؛ آنجا افزونهی Compose هم نصب میشود.
docker-compose یا docker compose؟
اول این ابهام را برطرف کنیم، چون در مثالهای اینترنت هر دو را میبینید:
docker-compose(با خط تیره) نسخهی اول کامپوز بود، برنامهای جدا که با پایتون نوشته شده بود. این نسخه از تابستان ۲۰۲۳ دیگر بهروزرسانی نمیگیرد.docker compose(با فاصله) نسخهی دوم است که با Go بازنویسی شده و به شکل افزونهی خود دستورdockerنصب میشود (بستهیdocker-compose-plugin).
فرمت فایل تقریباً یکی است و بیشتر فایلهای قدیمی بدون تغییر کار میکنند. دو تفاوت که به چشم میآید: اسم پیشفرض فایل حالا compose.yaml است (docker-compose.yml هم هنوز خوانده میشود)، و کلید version: در بالای فایل منسوخ شده و فقط یک هشدار تولید میکند؛ آن را ننویسید. در این نوشته همه جا از docker compose استفاده میکنیم.
مثال: برنامهی وب + PostgreSQL + Redis
ساختار پوشهی پروژه این است:
myapp/
├── compose.yaml
├── .env
├── Dockerfile
└── src/...
فرض میکنیم برنامهی شما یک Dockerfile دارد، روی پورت ۸۰۰۰ گوش میدهد و یک مسیر /health دارد که اگر برنامه سالم بود کد ۲۰۰ برمیگرداند. زبان و فریمورک مهم نیست؛ لاراول، جنگو یا Node فرقی نمیکند.
فایل .env
POSTGRES_DB=myapp
POSTGRES_USER=myapp
POSTGRES_PASSWORD=a-long-random-password
DB_HOST=db
DB_PORT=5432
REDIS_HOST=redis
این فایل رمز دارد؛ آن را در .gitignore بگذارید و هرگز commit نکنید. یک .env.example بدون مقدارهای واقعی کنار آن نگه دارید.
فایل compose.yaml
services:
app:
build: .
env_file: .env
ports:
- "127.0.0.1:8000:8000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
db:
image: postgres:17
env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
redis:
image: redis:7
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
pgdata:
redisdata:
حالا بخشبهبخش ببینیم هر چیز چه میکند.
سرویسها و شبکه
هر کلید زیر services یک سرویس است و کامپوز برای هر کدام یک کانتینر میسازد. همهی سرویسهای یک فایل بهطور خودکار در یک شبکهی مشترک قرار میگیرند و اسم سرویس همان hostname آن است. برای همین در .env نوشتیم DB_HOST=db: برنامه برای رسیدن به پایگاهداده به آدرس db:5432 وصل میشود، نه localhost. این رایجترین اشتباه کسانی است که تازه کامپوز را شروع میکنند؛ داخل کانتینر برنامه، localhost یعنی خود همان کانتینر.
دقت کنید که برای db و redis هیچ ports ننوشتیم. لازم نیست؛ برنامه از داخل شبکه به آنها میرسد و از بیرون سرور هم کسی نمیتواند به آنها وصل شود. برای خود برنامه هم پورت را فقط روی 127.0.0.1 منتشر کردیم. داکر برای پورتهای منتشرشده مستقیم قانون iptables میسازد و ufw را دور میزند، پس هر پورتی که بیآدرس منتشر کنید روی اینترنت باز است.
volumeهای نامدار: داده کجا میماند؟
کانتینر موقتی است. هر بار که آن را دوباره بسازید (مثلاً بعد از تغییر ایمیج)، هر چه داخلش نوشته شده بود از بین میرود. پایگاهداده نمیتواند اینطور باشد؛ برای همین مسیر دادهی PostgreSQL را به یک volume وصل کردیم:
volumes:
- pgdata:/var/lib/postgresql/data
pgdata یک volume نامدار است که در بخش volumes پایین فایل تعریف شده. داکر آن را در /var/lib/docker/volumes/ نگه میدارد و با پاکشدن کانتینر پاک نمیشود. اسم واقعیاش اسم پروژه بهعلاوهی اسم volume است، مثلاً myapp_pgdata:
docker volume ls
docker volume inspect myapp_pgdata
نوع دیگر، bind mount است که یک پوشهی مشخص از میزبان را داخل کانتینر میگذارد (./src:/app). bind mount برای کد در محیط توسعه خوب است، چون تغییرات فوراً دیده میشوند. برای دادهی پایگاهداده، volume نامدار انتخاب بهتری است: مسئلهی مالکیت و دسترسی فایلها پیش نمیآید و داکر خودش مدیریتش میکند.
یک نکته دربارهی نسخه: از PostgreSQL 18 به بعد، ایمیج رسمی مسیر داده را عوض کرده و volume باید به /var/lib/postgresql وصل شود. برای همین در مثال نسخه را صریح postgres:17 نوشتیم. همیشه نسخهی اصلی را pin کنید و از latest برای پایگاهداده استفاده نکنید؛ ارتقای نسخهی اصلی PostgreSQL مهاجرت داده لازم دارد و نباید با یک pull بیخبر اتفاق بیفتد.
و یادتان باشد: volume پشتیبان نیست. اگر دیسک خراب شود یا volume پاک شود، داده رفته است. از پایگاهداده جداگانه dump بگیرید:
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' | gzip > backup-$(date +%F).sql.gz
این دستور را میشود با cron زمانبندی کرد (کرون جاب در لینوکس) و نسخهها را طبق قاعدهی ۳-۲-۱ جای دیگری هم نگه داشت.
env_file و .env: دو کار متفاوت
اینجا دو مفهوم شبیه هم هست که بهتر است قاطی نشوند:
env_file: .envدر تعریف سرویس، متغیرها را داخل کانتینر میگذارد. PostgreSQL از رویPOSTGRES_USERوPOSTGRES_PASSWORDکاربر و رمز را میسازد و برنامه از رویDB_HOSTآدرس پایگاهداده را میخواند.- فایلی به اسم
.envکنارcompose.yaml، جدا از این، برای جایگذاری متغیر در خود فایل کامپوز هم خوانده میشود. یعنی اگر درcompose.yamlبنویسیدimage: postgres:${PG_VERSION}، مقدار از.envمیآید.
برای همین در healthcheck نوشتیم $${POSTGRES_USER} با دو علامت دلار. $$ یعنی «این را جایگذاری نکن»، تا متغیر دستنخورده به شل داخل کانتینر برسد و آنجا مقدار بگیرد.
healthcheck و depends_on
depends_on بهتنهایی فقط ترتیب شروع را تعیین میکند: کانتینر db اول ساخته میشود و بعد app. ولی «کانتینر شروع شد» با «PostgreSQL آمادهی پذیرش اتصال است» فرق دارد. PostgreSQL در اجرای اول چند ثانیه وقت میگذارد تا پوشهی داده را بسازد، و برنامهای که در این فاصله وصل شود خطا میگیرد.
راهحل همین دو بخش است که با هم کار میکنند:
healthcheckدستوری است که داکر مرتب داخل کانتینر اجرا میکند. برای PostgreSQL،pg_isreadyو برای Redis،redis-cli ping. اگر دستور با کد صفر تمام شود، کانتینرhealthyاست.condition: service_healthyدرdepends_onبه کامپوز میگویدappرا تا وقتیdbوredisسالم نشدهاند اجرا نکن.
پارامترهای healthcheck:
| پارامتر | معنی |
|---|---|
interval |
هر چند وقت یک بار بررسی شود |
timeout |
اگر دستور بیشتر از این طول کشید، شکست حساب شود |
retries |
بعد از چند شکست پشت سر هم، کانتینر unhealthy شود |
start_period |
مهلت اولیه که در آن شکستها شمرده نمیشوند |
healthcheck خود app به curl داخل ایمیج نیاز دارد. اگر ایمیج شما curl ندارد (مثلاً ایمیجهای slim یا distroless)، یا آن را در Dockerfile نصب کنید یا دستور دیگری بگذارید که در ایمیج موجود است.
وضعیت سلامت را در خروجی docker compose ps میبینید. یک نکتهی مهم: داکر کانتینر unhealthy را خودبهخود ریاستارت نمیکند؛ healthcheck فقط وضعیت را گزارش میدهد و برای depends_on استفاده میشود.
restart policy
restart: unless-stopped یعنی اگر کانتینر از کار افتاد یا سرور ریاستارت شد، داکر دوباره اجرایش کند، مگر اینکه خودتان آن را دستی متوقف کرده باشید. گزینههای دیگر no (پیشفرض)، always و on-failure هستند. برای سرویسهای همیشهروشن روی سرور، unless-stopped معمولاً انتخاب درستی است. شرطش این است که خود سرویس داکر روی بوت فعال باشد، که روی اوبونتو بهطور پیشفرض هست.
دستورهای روزمره
همهی این دستورها را در پوشهای که compose.yaml در آن است اجرا کنید:
docker compose up -d # ساختن و اجرای همهی سرویسها در پسزمینه
docker compose up -d --build # بعد از تغییر کد یا Dockerfile، ایمیج را دوباره بساز
docker compose ps # وضعیت سرویسها و سلامتشان
docker compose logs -f app # دنبالکردن زندهی لاگ یک سرویس
docker compose logs --tail=100 # صد خط آخر لاگ همهی سرویسها
docker compose exec db psql -U myapp myapp # اجرای دستور داخل کانتینر در حال اجرا
docker compose exec app sh # شل داخل کانتینر برنامه
docker compose restart app # ریاستارت یک سرویس
docker compose pull # گرفتن نسخهی تازهی ایمیجها
docker compose config # فایل نهایی بعد از جایگذاری متغیرها؛ برای پیدا کردن خطا
docker compose down # توقف و حذف کانتینرها و شبکه
فرق stop و down: stop فقط کانتینرها را متوقف میکند و آنها سر جایشان میمانند؛ down آنها را حذف میکند و شبکه را هم برمیدارد. در هر دو حالت volumeها دستنخورده میمانند و داده از بین نمیرود.
خطر docker compose down -v
docker compose down -v
پرچم -v volumeهای نامدار پروژه را هم پاک میکند. یعنی pgdata و هر چه در پایگاهداده بود، بدون هیچ پرسشی از بین میرود. در محیط توسعه، وقتی میخواهید از صفر شروع کنید، دستور مفیدی است. روی سرور واقعی، تقریباً هیچوقت آن را لازم ندارید. اگر لازم شد، اول مطمئن شوید پشتیبان سالم و تازه دارید.
یک چکلیست کوتاه برای کامپوز روی سرور
- نسخهی ایمیجها را pin کنید (
postgres:17، نهpostgresیاlatest). - فقط پورتهایی را منتشر کنید که واقعاً از بیرون لازماند، و تا جای ممکن روی
127.0.0.1. - رمزها در
.envباشند و.envدر git نباشد. - برای هر سرویسی که داده دارد volume نامدار تعریف کنید و جداگانه از آن پشتیبان بگیرید.
- برای پایگاهداده و کش healthcheck بگذارید و در
depends_onازservice_healthyاستفاده کنید. restart: unless-stoppedرا فراموش نکنید.
جمعبندی
Docker Compose یعنی کل محیط برنامه در یک فایل خوانا که میشود آن را در git نگه داشت، بررسی کرد و روی هر سرور دیگری دوباره ساخت. در مثال این نوشته، برنامه از روی اسم سرویس به پایگاهداده و Redis وصل میشود، داده در volumeهای نامدار میماند، و healthcheck همراه depends_on باعث میشود برنامه پیش از آمادهشدن پایگاهداده اجرا نشود. دستورهای روزمره همین چند تا هستند: up -d، ps، logs -f، exec و down. فقط پرچم -v را جدی بگیرید و پیش از زدنش یک بار دیگر فکر کنید.