Docker Compose در محیط Production: ۱۰ اشتباه رایج و راهحل هر کدام
Docker Compose برای خیلی از تیمها ابزار کاملاً مناسبی برای Production است: ساده، قابل فهم و بدون پیچیدگی کوبرنتیز. اما فایل Composeی که روی لپتاپ توسعهدهنده کار میکند، معمولاً برای سرور واقعی آماده نیست. در این مقاله ده اشتباهی را مرور میکنیم که در بررسی سرورها بارها دیدهایم، همراه با راهحل هر کدام.
۱. استفاده از تگ latest
مشکل: image: postgres:latest یعنی هر بار که ایمیج دوباره دریافت شود، ممکن است نسخهی اصلی عوض شود. یک docker compose pull ساده میتواند PostgreSQL را یک نسخهی اصلی بالا ببرد و دیتابیس با دادههای قبلی اجرا نشود. برای ایمیجهای خودتان هم latest یعنی نمیدانید الان چه نسخهای اجرا میشود و برگشت به نسخهی قبل سخت است.
راهحل: نسخه را صریح بنویسید و برای ایمیجهای خودتان از تگ نسخه یا شناسهی Commit استفاده کنید. برای حساسترین موارد، Digest را هم قفل کنید:
services:
db:
image: postgres:17.6
app:
image: registry.example.com/shop/app:2026.10.3
proxy:
image: nginx:1.27-alpine@sha256:<digest>
۲. اسرار در فایل env داخل مخزن گیت
مشکل: فایل .env با رمز دیتابیس و کلیدهای API کامیت شده است. هر کسی که به مخزن دسترسی دارد (یا داشته است) به رمزها هم دسترسی دارد، و حذف فایل از آخرین Commit آن را از تاریخچهی گیت پاک نمیکند.
راهحل: فقط .env.example را در مخزن نگه دارید، .env را در .gitignore بگذارید و فایل واقعی را با دسترسی محدود روی سرور بسازید. برای اسرار حساستر از Secrets در Compose استفاده کنید که بهصورت فایل داخل کانتینر قرار میگیرد و در docker inspect دیده نمیشود:
services:
db:
image: postgres:17.6
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
chmod 600 .env secrets/db_password.txt
اگر رمزی قبلاً کامیت شده، آن را لو رفته فرض کنید و عوض کنید.
۳. نبود سیاست ریاستارت
مشکل: بدون restart، اگر پروسهی اپلیکیشن خطا بدهد و خارج شود یا سرور ریبوت شود، کانتینر خاموش میماند تا کسی متوجه شود.
راهحل:
services:
app:
restart: unless-stopped
با unless-stopped، کانتینر بعد از خطا و ریبوت سرور دوباره بالا میآید، مگر اینکه خودتان آن را دستی متوقف کرده باشید. مطمئن شوید سرویس Docker هم در بوت فعال است: systemctl is-enabled docker.
۴. نبود Health Check و depends_on بدون شرط
مشکل: depends_on: [db] فقط تضمین میکند کانتینر دیتابیس شروع شده باشد، نه اینکه آمادهی پذیرش اتصال باشد. اپلیکیشن زودتر بالا میآید، به دیتابیس وصل نمیشود و خطا میدهد.
راهحل: برای سرویسها Health Check تعریف کنید و وابستگی را مشروط کنید:
services:
db:
image: postgres:17.6
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 10
app:
depends_on:
db:
condition: service_healthy
حالا docker compose ps وضعیت healthy یا unhealthy را هم نشان میدهد که برای مانیتورینگ هم مفید است. یادتان باشد که Docker بهتنهایی کانتینر unhealthy را ریاستارت نمیکند؛ این وضعیت را در مانیتورینگ هشدار کنید.
۵. باز کردن پورت دیتابیس روی اینترنت (و اعتماد به ufw)
مشکل: این یکی از خطرناکترین اشتباههاست. فایل Compose این خط را دارد:
ports:
- "5432:5432"
و مدیر سرور با خیال راحت میگوید «ufw فقط پورتهای ۲۲، ۸۰ و ۴۴۳ را باز گذاشته». اما Docker فایروال ufw را دور میزند. دلیلش این است که Docker برای پورتهای منتشرشده، قوانین iptables خودش را در جدول nat (برای DNAT) و زنجیرهی FORWARD اضافه میکند. ترافیکی که به کانتینر میرود از نظر کرنل «عبوری» است، نه «ورودی» به خود سرور؛ پس از زنجیرهی INPUT که ufw قوانینش را آنجا میگذارد عبور نمیکند. نتیجه: پورت دیتابیس روی همهی اینترنت باز است، در حالی که ufw status چیز دیگری نشان میدهد.
راهحل:
- دیتابیس و Redis معمولاً اصلاً به پورت منتشرشده نیاز ندارند. اپلیکیشن از طریق شبکهی داخلی Compose با نام سرویس (
db:5432) به آن وصل میشود. خطportsرا حذف کنید. - اگر از خود سرور (مثلاً برای ابزار مدیریت) به آن نیاز دارید، فقط روی localhost منتشر کنید و از طریق تونل SSH وصل شوید:
ports:
- "127.0.0.1:5432:5432"
ssh -L 5432:127.0.0.1:5432 user@server
- اگر واقعاً باید پورتی را برای IPهای مشخص باز کنید، قوانین محدودکننده را در زنجیرهی
DOCKER-USERبگذارید که Docker قبل از قوانین خودش بررسی میکند.
برای بررسی، از یک سیستم بیرونی تست کنید: nc -zv SERVER_IP 5432. نکات بیشتر در چکلیست امنیت کانتینرهای Docker آمده است.
۶. نبود محدودیت منابع
مشکل: یک نشت حافظه در یک سرویس میتواند همهی RAM سرور را مصرف کند؛ OOM Killer کرنل وارد عمل میشود و ممکن است پروسهی دیتابیس را بکشد، نه سرویس مقصر را.
راهحل: برای هر سرویس سقف حافظه و CPU بگذارید. Docker Compose نسخهی ۲ بخش deploy.resources را بدون Swarm هم اعمال میکند:
services:
app:
deploy:
resources:
limits:
cpus: "1.5"
memory: 1G
reservations:
memory: 512M
سقفها را بر اساس مصرف واقعی تعیین کنید (docker stats)، نه حدس. سقف خیلی پایین یعنی کانتینر مدام به خاطر کمبود حافظه کشته میشود.
۷. لاگهای بیسقف
مشکل: درایور پیشفرض لاگ Docker (json-file) بهطور پیشفرض محدودیت اندازه ندارد. یک اپلیکیشن پرحرف در چند هفته دهها گیگابایت لاگ در /var/lib/docker/containers میسازد و دیسک پر میشود. (اگر با این وضعیت روبهرو هستید، راهنمای رفع پر شدن دیسک لینوکس را ببینید.)
راهحل: چرخش لاگ را برای کل Docker در /etc/docker/daemon.json تنظیم کنید:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
sudo systemctl restart docker
این تنظیم فقط روی کانتینرهایی اعمال میشود که بعد از آن ساخته شوند؛ کانتینرهای فعلی را با docker compose up -d --force-recreate دوباره بسازید. یا همین تنظیم را برای هر سرویس در Compose بنویسید:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
۸. داده داخل کانتینر بهجای Volume
مشکل: دادهای که در لایهی قابلنوشتن کانتینر ذخیره شود، با docker compose down یا ساخت دوبارهی کانتینر (مثلاً هنگام بهروزرسانی ایمیج) از بین میرود. دیتابیس بدون Volume، یا پوشهی آپلود بدون Volume، یک فاجعهی در انتظار است.
راهحل: هر مسیری که دادهی پایدار دارد را به Volume نامدار یا Bind Mount وصل کنید:
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
برای پیدا کردن مسیرهایی که دادهی پایدار دارند، مستندات ایمیج را بخوانید (معمولاً در بخش VOLUME در Dockerfile آن هم مشخص است). و هرگز روی سرور Production دستور docker compose down -v را اجرا نکنید؛ گزینهی -v Volumeها را هم حذف میکند.
۹. Volume داریم، پس بکاپ داریم؟ نه!
مشکل: Volume داده را در برابر ساخت دوبارهی کانتینر حفظ میکند، نه در برابر خرابی دیسک، حذف اشتباهی، باجافزار یا خراب شدن دیتابیس. کپی کردن مستقیم پوشهی دیتابیس در حال اجرا هم بکاپ سالمی نمیدهد.
راهحل: از ابزار خود دیتابیس بکاپ بگیرید و آن را خارج از سرور نگه دارید:
docker compose exec -T db pg_dump -U app -Fc appdb > /backup/appdb-$(date +%F).dump
این دستور را با cron زمانبندی کنید و فایلها را با ابزاری مثل restic به یک Object Storage بفرستید (آموزش آن در بکاپ با restic روی Object Storage). مهمتر از همه: بازیابی را بهصورت دورهای روی یک سرور تست امتحان کنید. بکاپی که بازیابیاش تست نشده، بکاپ نیست.
۱۰. اجرای کانتینرها با root
مشکل: بسیاری از ایمیجها بهطور پیشفرض با کاربر root اجرا میشوند. اگر آسیبپذیریای در اپلیکیشن وجود داشته باشد، مهاجم داخل کانتینر root است و راههای بیشتری برای فرار به میزبان یا دستکاری فایلهای Volume دارد.
راهحل: در Dockerfile کاربر غیر root تعریف کنید (نمونهها را در راهنمای داکرایز کردن لاراول، جنگو و Node.js ببینید)، یا در Compose کاربر را مشخص کنید و دسترسیها را کم کنید:
services:
app:
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
read_only و cap_drop را بعد از تست اعمال کنید؛ بعضی اپلیکیشنها به نوشتن در مسیرهای مشخص یا Capability خاصی نیاز دارند که باید آنها را جداگانه اجازه دهید.
چکلیست سریع
| مورد | بررسی |
|---|---|
| نسخهی ایمیجها | هیچ latestی در فایل نیست |
| اسرار | .env در گیت نیست؛ دسترسی فایل 600 |
| ریاستارت | همهی سرویسها restart دارند |
| آمادگی | Health Check و condition: service_healthy |
| پورتها | دیتابیس و Redis پورت عمومی ندارند |
| منابع | سقف حافظه و CPU تعریف شده |
| لاگ | max-size و max-file تنظیم شده |
| داده | همهی مسیرهای پایدار روی Volume |
| بکاپ | خارج از سرور، زمانبندیشده و تستشده |
| کاربر | کانتینرها با کاربر غیر root |
جمعبندی
هیچکدام از این ده مورد پیچیده نیست، اما هر کدام میتواند به قطعی، نشت داده یا از دست رفتن اطلاعات منجر شود. اگر سرویس شما از چند سرور فراتر رفته و به Self-healing و استقرار بدون قطعی در مقیاس بالا نیاز دارد، مقالهی چه زمانی از Docker Compose به کوبرنتیز مهاجرت کنیم؟ را بخوانید.
نمیخواهید این کار را خودتان انجام دهید؟ تیم مروانت فایلهای Compose و سرورهای Docker شما را بررسی میکند، این مشکلات را رفع میکند و بکاپ و مانیتورینگ را راه میاندازد. صفحهی برونسپاری پروژههای Docker را ببینید.
