تفاوتهای اجرای Docker روی VPS در مقایسه با سیستم شخصی
اجرای Docker روی VPS با چالشهای خاصی همراه است. در این مطلب بررسی میکنیم چرا پورتها از UFW عبور میکنند، کانتینرها پس از reboot متوقف میشوند و دیسک به سرعت پر میشود.
تغییرات هنگام اجرای Docker روی یک VPS
Docker روی یک VPS همان موتور و همان imageهایی را اجرا میکند که روی لپتاپ شما اجرا میشوند، بنابراین تمام دستوراتی که میدانید همچنان کار میکنند. آنچه تغییر میکند، فضای اطراف آن است. لپتاپ حافظه اضافی دارد، فایروالی دارد که کسی آن را اسکن نمیکند و دیسکی به قدری بزرگ که هرگز به آن نگاه نمیکنید. یک سرور اجارهای سقف حافظه مشخصی دارد، یک IP عمومی دارد که چند دقیقه پس از بالا آمدن اسکن میشود و یک root filesystem دارد که Docker بدون پرسش آن را پر میکند.
چهار تفاوت باعث بروز بیشتر مشکلات در یک سرور کوچک میشوند:
- حافظه محدود است و هسته سیستمعامل (kernel) کمبود آن را با کشتن یک پردازش حل میکند.
- یک پورت منتشر شده مستقیماً از UFW (uncomplicated firewall) عبور میکند، زیرا Docker قوانین فایروال مخصوص به خود را مینویسد.
- کانتینرها پس از reboot باز نمیگردند مگر اینکه از قبل آن را درخواست کرده باشید.
- imageها، کانتینرها، volumeها و build cache تا زمانی که دیسک پر شود، رشد میکنند.
هر بخش در ادامه، نوع خطا، متنی که واقعاً مشاهده خواهید کرد و راهنمایی که آن را به طور عمیق حل میکند، نام میبرد. اگر هنوز یک compose file ننوشتهاید، ابتدا اصول Docker Compose روی یک VPS را بخوانید و سپس بازگردید. این صفحه فرض میکند که شما از قبل میتوانید یک stack را بالا بیاورید.
یک کانتینر Docker چقدر RAM مصرف میکند؟
کمتر از آنچه اکثر افراد تصور میکنند. یک کانتینر در واقع یک پردازش درون یک cgroup (گروه کنترل) است، نه یک ماشین مجازی؛ بنابراین هسته مهمان (guest kernel) وجود ندارد و تخصیص حافظه ثابت نیست. هزینه حافظه، دقیقاً به میزان استفادهٔ پردازش داخلی بستگی دارد. به همین دلیل است که یک پشته کامل در 2 GB جا میشود، در حالی که همان پشته اگر با ماشینهای مجازی ساخته میشد، چنین وضعیتی نداشت.
ارقام زیر، مقادیر معمول در حالت idle برای ایمیجهای استاندارد روی Ubuntu 24.04 با پیکربندی پیشفرض هستند که از docker stats چند دقیقه پس از شروع کار خوانده شدهاند. اینها نقطه شروعی برای برنامهریزی هستند، نه بنچمارکی برای بار کاری شما. پیش از اعتماد به هر رقمی، از جمله این موارد، docker stats --no-stream را روی سیستم خود اجرا کنید.
The data behind this chart
[
{
"label": "nginx",
"idle_mb": 8,
"budget_mb": 64
},
{
"label": "Redis 7",
"idle_mb": 12,
"budget_mb": 128
},
{
"label": "Traefik v3",
"idle_mb": 40,
"budget_mb": 128
},
{
"label": "PostgreSQL 16",
"idle_mb": 45,
"budget_mb": 512
},
{
"label": "Uptime Kuma",
"idle_mb": 95,
"budget_mb": 256
},
{
"label": "MariaDB 11",
"idle_mb": 190,
"budget_mb": 512
},
{
"label": "Nextcloud (Apache)",
"idle_mb": 210,
"budget_mb": 768
}
]این دو ستون وظایف متفاوتی دارند. idle_mb میزان مصرف کانتینر در حالتی است که هیچ کاری انجام نمیدهد. budget_mb مقداری است که باید هنگام برنامهریزی رزرو کنید، زیرا استفاده واقعی هرگز در حالت idle باقی نمیماند. PostgreSQL در حالت idle نزدیک به 45 MB مصرف دارد و پس از فعال شدن اتصالات، مرتبسازیها و کش، به 512 MB نیاز خواهد داشت. برنامهریزی را با ستون بودجه (budget) انجام دهید و عیبیابی را با ستون idle.
به ساختار آن 7 ردیف دقت کنید. nginx در حالت idle حدود 8 MB و Nextcloud حدود 210 MB مصرف دارند. پروکسی که جلوی برنامههای شما قرار میگیرد تقریباً رایگان است. پایگاه داده و برنامه PHP همان مواردی هستند که باید بر اساس آنها اندازه سرور را تعیین کنید.
یک هشدار درباره docker stats: رقم حافظه شامل page cacheای است که توسط خواندن فایلهای خود کانتینر ایجاد شده است؛ بنابراین این مقدار پس از شروع کار برای مدتی افزایش مییابد و سپس ثابت میشود. پیش از آنکه نتیجه بگیرید چیزی دچار نشت حافظه (leak) شده است، آن را به مدت یک ساعت زیر نظر بگیرید.
تعیین اندازه VPS: چه چیزی در 2 GB، 4 GB و 8 GB جا میشود
ابتدا سهم میزبان را از کل حافظه کسر کنید. هسته سیستمعامل، systemd، journald، sshd و Docker daemon همگی در همان RAMای قرار دارند که کانتینرهای شما در آن اجرا میشوند و dockerd به همراه containerd حدود 100 MB از آن را اشغال میکنند. شما همچنین به حافظه آزاد برای page cache و برای لحظات اوج مصرف هنگام build کردن image یا dump گرفتن از دیتابیس نیاز دارید.
The data behind this chart
[
{
"plan": "2 GB VPS",
"total_mb": 2048,
"host_reserve_mb": 768,
"container_mb": 1280
},
{
"plan": "4 GB VPS",
"total_mb": 4096,
"host_reserve_mb": 1024,
"container_mb": 3072
},
{
"plan": "8 GB VPS",
"total_mb": 8192,
"host_reserve_mb": 1536,
"container_mb": 6656
}
]host_reserve_mb سیستمعامل، Docker daemon و فضای خالی لازم برای حفظ پاسخدهی سرور تحت بار را پوشش میدهد. آنچه باقی میماند container_mb است و این تنها عددی است که میتوانید برای سرویسها هزینه کنید. این ذخیره با افزایش پلن بزرگتر میشود، از 768 MB در کوچکترین سرور تا 1536 MB در بزرگترین سرور؛ زیرا سرور بزرگتر کانتینرهای بیشتری اجرا میکند، لاگهای بیشتری مینویسد و به page cache بیشتری نیاز دارد.
یک پلن 2 GB مقدار 1280 MB برای کانتینرها باقی میگذارد. اگر 512 MB از آن را صرف PostgreSQL و 128 MB را صرف Traefik کنید، نیمی از آن تمام شده است. باقیمانده برای دو برنامه کوچک با حدود 256 MB برای هر کدام کافی است. این یک سرور واقعی و کاربردی است. اما فضایی برای Nextcloud و یک کلاستر جستجو در کنار آنها وجود ندارد.
یک پلن 4 GB مقدار 3072 MB باقی میگذارد که در آن یک دیتابیس، یک reverse proxy، سه برنامه و یک کانتینر مانیتورینگ همگی بهطور همزمان جا میشوند. این کوچکترین اندازهای است که ارزش استفاده برای کارهای مهم را دارد، زیرا حافظه اضافی همان چیزی است که اثر یک deploy ناموفق را خنثی میکند.
یک پلن 8 GB مقدار 6656 MB از کل 8192 MB خود را باقی میگذارد و در این سطح، محدودیت معمولاً از حافظه به CPU یا پهنای باند دیسک منتقل میشود. اگر محاسبات نشان میدهد که stack شما جا نمیشود، بهجای تلاش برای بهینهسازیهای پیچیده، پلن بزرگتر را تهیه کنید: هزینه واقعی یک VPS توضیح میدهد که گیگابایتهای اضافی در ماه چه ارزشی دارند.
دو قانون محاسبات را دقیق نگه میدارند. برای هر سرویس یک محدودیت حافظه (memory limit) تعیین کنید تا یک پردازش سرکش نتواند کل سرور را از کار بیندازد. و بخشی از بودجه حافظه را بدون استفاده باقی بگذارید، زیرا docker compose build و pg_dump هر دو در بدترین زمان ممکن به حافظه نیاز پیدا میکنند. محدودیتهای حافظه در Docker Compose سینتکس و تلههای این کار را توضیح میدهد.
چرا کانتینر من با کد 137 متوقف میشود؟
زیرا هسته سیستمعامل (kernel) آن را متوقف کرده است. کد 137 حاصل جمع 128 و 9 است و سیگنال 9 همان SIGKILL میباشد. کانتینر حافظهای بیش از حد مجاز درخواست کرده و در نتیجه توسط OOM killer (قاتل حافظه) متوقف شده است.
CONTAINER ID IMAGE STATUS
9f2c1a4d7b3e postgres:16 Exited (137) 4 minutes agoبهجای حدسزدن، علت را تأیید کنید:
docker inspect my-db | grep -i oomkilled
sudo journalctl -k | grep -i "out of memory"عبارت "OOMKilled": true نشان میدهد که کانتینر به محدودیت cgroup خود رسیده است و لاگ هسته، پردازشی را که انتخاب کرده است نام میبرد:
Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kBاین حالت مطلوب است، زیرا آسیب فقط به یک کانتینر محدود شده است. حالت بد زمانی است که کانتینر هیچ محدودیتی نداشته باشد. بدون محدودیت، سقف مصرف کانتینر کل ماشین است؛ بنابراین نشت حافظه در یک سرویس، منابع میزبان را تخلیه میکند و هسته مجبور میشود قربانی را بر اساس اندازه در کل سیستم انتخاب کند. در این حالت، خط لاگ فاقد پیشوند Memory cgroup است و به صورت Out of memory: Killed process 2417 (postgres) نمایش داده میشود. پردازشی که هسته انتخاب میکند اغلب دیتابیس شماست، در حالی که کانتینری که دچار نشت حافظه شده همچنان به کار خود ادامه میدهد. به همین دلیل است که تعیین محدودیت برای هر سرویس، از مقدار دقیق هر محدودیت مهمتر است.
استفاده از Swap زمانبندی را تغییر میدهد، نه محاسبات را. اکثر ایمیجهای VPS بدون Swap عرضه میشوند. با دستور swapon --show بررسی کنید؛ اگر Swap وجود نداشته باشد، این دستور هیچ خروجیای نمایش نمیدهد. یک فایل Swap به هسته فضایی میدهد تا صفحات حافظه غیرفعال را به آن منتقل کند؛ این کار به شما چند دقیقه زمان میخرد تا متوجه مشکل شوید.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -hاکنون free -h باید یک مقدار غیر صفر در ردیف Swap نشان دهد. Swap به معنای افزایش RAM نیست. سیستمی که تحت فشار مداوم حافظه باشد، آنقدر کند میشود که دیگر نمیتوانید برای رفع مشکل از طریق SSH وارد آن شوید؛ بنابراین به Swap به چشم یک بافر هشدار نگاه کنید و اندازه آن را اصلاح کنید.
چرا UFW پورتهای منتشرشده توسط Docker را مسدود نمیکند؟
دلیل این است که ترافیک هرگز به زنجیرهای که UFW از آن محافظت میکند، نمیرسد. هنگامی که یک پورت را با -p 5432:5432 یا یک ورودی ports: در فایل compose منتشر میکنید، دیمون Docker یک قانون DNAT (ترجمه آدرس شبکه مقصد) در جدول nat و یک قانون accept در زنجیره اختصاصی DOCKER خود مینویسد. بستهای که به مقصد یک کانتینر ارسال میشود، بهجای تحویل به میزبان، به آن کانتینر فوروارد میشود؛ بنابراین در مسیر FORWARD پردازش شده و هرگز از قوانین INPUT که UFW مینویسد عبور نمیکند.
شما میتوانید این اتفاق را روی سرور مشاهده کنید:
sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432UFW ممکن است وضعیت 5432 DENY IN Anywhere را نشان دهد، در حالی که جدول nat حاوی یک قانون DNAT tcp ... to:172.18.0.2:5432 برای همان پورت است. از یک ماشین دیگر، nc -vz your.server.ip 5432 همچنان متصل میشود. دیتابیس روی اینترنت عمومی قرار دارد، اما فایروال میگوید که اینطور نیست.
راه حل این است که پورتهای کمتری را منتشر کنید. کانتینرها در یک پروژه compose از یک شبکه مشترک استفاده میکنند و از طریق نام سرویس به یکدیگر دسترسی دارند؛ بنابراین دیتابیسی که فقط به اپلیکیشن کنار خود سرویس میدهد، اصلاً نیازی به ورودی ports: ندارد. زمانی که به دسترسی محلی نیاز دارید، انتشار پورت را به loopback محدود کنید:
services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"پس از docker compose up -d، دستور nc -vz your.server.ip 5432 از خارج از شبکه با شکست مواجه میشود، اما psql -h 127.0.0.1 -p 5432 روی همان سیستم همچنان کار میکند. در یک پشته (stack) کوچک و سالم، فقط reverse proxy باید پورتهای 80 و 443 را منتشر کند. مطلب چرا پورتهای منتشرشده توسط Docker از UFW عبور میکنند زنجیره DOCKER-USER را برای مواردی که مجبور به انتشار پورت و فیلتر کردن آن هستید بررسی میکند، و اصول فایروال UFW قوانین پایه میزبان را پوشش میدهد.
چرا کانتینرهای من پس از reboot ناپدید میشوند؟
زیرا هیچ دستوری برای بازگشت آنها صادر نشده است. یک کانتینر بهصورت پیشفرض با سیاست restart policy از نوع no ایجاد میشود مگر آنکه خودتان تنظیمات دیگری اعمال کنید؛ بنابراین پس از reboot، کانتینر در وضعیت متوقف باقی میماند و daemon نیز اقدامی برای آن انجام نمیدهد. reboot شدن در VPSها اتفاق نادری نیست: بهروزرسانیهای هسته (kernel) توسط unattended upgrades، تعمیرات نگهداری توسط سرویسدهنده و سناریوی OOM که پیشتر ذکر شد، همگی منجر به reboot میشوند.
دو شرط باید برقرار باشد. ابتدا daemon باید در زمان بوت شروع به کار کند:
systemctl is-enabled dockerاین دستور در یک نصب استاندارد Ubuntu عبارت enabled را چاپ میکند. سپس هر سرویس باید یک سیاست مشخص داشته باشد:
services:
app:
image: ghcr.io/example/app:1.4
restart: unless-stoppedگزینه unless-stopped کانتینر را پس از reboot بازمیگرداند و به کانتینرهایی که عمداً متوقف کردهاید احترام میگذارد. گزینه always حتی کانتینرهایی را که عمداً متوقف کردهاید نیز در هر بار راهاندازی مجدد daemon اجرا میکند که این موضوع در حین عیبیابی میتواند غافلگیرکننده باشد. ویرایش فایل به تنهایی کافی نیست، زیرا سیاست restart policy در لحظه ایجاد کانتینر تعیین میشود. دستور docker compose up -d را اجرا کنید تا کانتینر دوباره ساخته شود، سپس مقدار فعال آن را بررسی کنید:
docker inspect my-app | grep -A3 RestartPolicyسپس سرور را عمداً reboot کنید و در دایرکتوری پروژه دستور docker compose ps را اجرا کنید. استکی که از یک reboot برنامهریزیشده جان سالم به در ببرد، در برابر rebootهای ناگهانی نیز مقاوم است. اگر استک شما به ترتیب خاصی برای اجرا یا یک job یکباره در زمان بوت نیاز دارد، یک unit در systemd ابزار بهتری است: اجرای Docker Compose در زمان بوت شامل فایل unit مربوطه است. برای اینکه بدانید آیا کانتینری که بازگشته واقعاً در حال سرویسدهی است یا خیر، از Compose healthchecks استفاده کنید.
چرا دیسک VPS من پر شده است؟
دلیل این است که Docker همه چیز را تا زمانی که دستور حذف ندهید، نگه میدارد. هر تگ image که تا به حال pull کردهاید، هر container متوقفشده، هر volume بینام (anonymous) که پس از بازسازی (recreate) باقی مانده و تمام لایههای build cache روی دیسک میمانند. در یک فایلسیستم root با حجم 40 GB یا 80 GB که برای این پلنها معمول است، این وضعیت در عرض چند ماه (نه چند سال) منجر به قطعی سرویس میشود.
پر شدن دیسک مانند یک crash به نظر نمیرسد. شما در یک ساعت مشابه، خطای no space left on device را از یک container، از apt، از journald و از docker pull دریافت میکنید. PostgreSQL دیگر نوشتن را نمیپذیرد. سرور همچنان روشن است، که باعث میشود تشخیص آن از یک حلقه reboot دشوارتر باشد.
پیش از حذف، بررسی کنید:
docker system df
df -h /دستور docker system df مجموع فضا را به تفکیک images، containers، local volumes و build cache نشان میدهد و در کنار هر کدام ستون RECLAIMABLE قرار دارد. در سروری که خودش imageها را build میکند، build cache معمولاً بزرگترین بخش است.
docker image prune -a
docker builder prune
docker system dfدستور docker image prune -a تمام imageهایی که توسط هیچ container استفاده نمیشوند را حذف میکند. docker builder prune حافظه build cache را پاک میکند. هر دو دستور در حین اجرای سرویسها ایمن هستند، زیرا موارد در حال استفاده نادیده گرفته میشوند. آنچه ایمن نیست docker system prune --volumes است که هر volume که در حال حاضر توسط هیچ container ارجاع داده نشده را حذف میکند. یک stack که برای آخر هفته متوقف کردهاید دقیقاً همین وضعیت را دارد و volume دیتابیس آن نیز حذف خواهد شد. پیش از تایپ این flag، مطلب bind mounts versus named volumes را بخوانید و ابتدا یک نسخه پشتیبان تهیه کنید.
لاگهای container رشد بیصداتری دارند. درایور پیشفرض json-file هیچ محدودیتی برای اندازه ندارد، بنابراین یک container پرحرف، گیگابایتها داده در /var/lib/docker/containers مینویسد. برای هر container در /etc/docker/daemon.json محدودیت اعمال کنید:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}آن را با sudo systemctl restart docker اعمال کنید؛ این دستور containerهای شما را restart میکند، پس زمان مناسبی را انتخاب کنید. این محدودیت برای containerهایی که پس از تغییر ایجاد میشوند اعمال میشود، بنابراین containerهای در حال اجرا را با docker compose up -d --force-recreate بازسازی (recreate) کنید و تأیید نمایید:
docker inspect my-app | grep -A5 LogConfig
sudo du -sh /var/lib/docker/containersخروجی inspect باید مقدار max-size تنظیمشده را نشان دهد. اگر خالی باشد، آن container پیش از اعمال تغییر ایجاد شده و همچنان بدون محدودیت در حال نوشتن است.
عادتهایی برای حفظ سلامت یک سرور کوچک Docker
هیچکدام از این موارد نیازی به داشبورد یا ابزاری که مجبور به یادگیری آن باشید، ندارند.
- در روز اول هر ماه دستورات
docker system dfوdf -h /را اجرا کنید. دو دستور، سی ثانیه زمان، و شما روند تغییرات را بسیار پیش از آنکه به قطعی منجر شود، مشاهده خواهید کرد. - برای هر سرویس یک محدودیت حافظه (memory limit) تعیین کنید، حتی برای آنهایی که مطمئن هستید کوچک هستند. این محدودیت باعث میشود یک قطعی در سطح کل میزبان، به ریاستارت شدن تنها یک container تبدیل شود.
- وضعیت سرور را از جایی دیگر مانیتور کنید تا پیش از آنکه kernel وارد عمل شود، از فشار روی حافظه یا دیسک مطلع شوید. Uptime Kuma در یک container اجرا میشود و در حالت idle حدود 95 MB حافظه مصرف میکند.
- از volumeها نسخه پشتیبان تهیه کنید، نه از containerها. container قابل دور ریختن است اما volume خیر. restic backups on a VPS یک برنامه زمانبندی و تست بازیابی را پوشش میدهد.
- تگهای image را در فایل compose ثابت (pin) کنید و آنها را در روزی که خودتان انتخاب میکنید بهروزرسانی کنید. با
latest، نسخهای که درdocker compose pullبعدی دریافت میکنید، همان نسخهای خواهد بود که در آن صبح منتشر شده است.
یک VPS کوچک که Docker را اجرا میکند، زمانی سالها سالم میماند که چهار عدد در محدوده مجاز باقی بمانند: بودجه حافظه، لیست پورتهای منتشر شده، سیاست ریاستارت برای هر سرویس، و فضای آزاد دیسک. باقی موارد همان Docker است که همین حالا در خانه از آن استفاده میکنید.
FAQ
How much RAM do I need to run Docker on a VPS?
Docker itself is cheap. The daemon and containerd together sit near 100 MB, and the rest of the requirement is your containers. Budget the host's share first: 768 MB on a 2048 MB box for the operating system, the daemon and headroom, which leaves 1280 MB to spend. A database at 512 MB, a reverse proxy at 128 MB and two small applications fit inside that. Measure your own stack with docker stats --no-stream rather than trusting any published figure.
Can I run Docker on a 1 GB VPS?
Yes, for one or two light containers, and add a swap file before you start. Roughly half of a 1 GB box is gone once the operating system and the Docker daemon are running, which leaves room for a small application and a reverse proxy, but not for a database under real load. Building images on a box that size will fail or kill something else, so build elsewhere and pull the finished image.
Does UFW protect a Docker container?
Not for ports you publish. Docker writes its own DNAT and forward rules, so a packet aimed at a published container port is forwarded to the container instead of being delivered to the host, and the INPUT rules UFW manages never see it. ufw deny 5432 can be active while that port answers from the internet. Publish to loopback with 127.0.0.1:5432:5432, leave internal services unpublished, or filter in the DOCKER-USER chain.
Will my containers restart after a VPS reboot?
Only if they were created with a restart policy. Set restart: unless-stopped on each service, run docker compose up -d so the containers are recreated with it, and confirm systemctl is-enabled docker prints enabled. Then reboot on purpose and check docker compose ps. A restart policy you have never tested is not a restart policy.
How often should I prune Docker images?
Monthly is enough for most small servers, or whenever docker system df reports reclaimable space you would miss. docker image prune -a and docker builder prune are both safe while services run, because images and cache in use are skipped. Avoid docker system prune --volumes unless you know exactly which volumes are unreferenced, because it deletes the data of any stack that happens to be stopped.