SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-27

تفاوت‌های اجرای 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 ننوشته‌اید، ابتدا اصول Docker Compose روی VPS را بخوانید و سپس بازگردید. این صفحه فرض می‌کند که شما از قبل توانایی بالا آوردن یک stack را دارید.

یک کانتینر Docker چقدر رم مصرف می‌کند؟

کمتر از آن چیزی که اکثر افراد تصور می‌کنند. یک کانتینر در واقع یک پردازش درون یک cgroup (گروه کنترل) است، نه یک ماشین مجازی؛ بنابراین هسته مهمان (guest kernel) وجود ندارد و تخصیص حافظه ثابت نیست. هزینه حافظه، دقیقاً به میزان استفاده پردازش داخلی بستگی دارد. به همین دلیل است که یک پشته کامل (full stack) در 2 GB جا می‌شود، در حالی که همان پشته اگر با ماشین‌های مجازی ساخته می‌شد، چنین نبود.

ارقام زیر، مقادیر معمول در حالت idle برای ایمیج‌های استاندارد روی Ubuntu 24.04 با پیکربندی پیش‌فرض هستند که docker stats چند دقیقه پس از شروع کار خوانده شده‌اند. این ارقام صرفاً نقطه شروعی برای برنامه‌ریزی هستند و نه یک بنچمارک برای بار کاری شما. پیش از اعتماد به هر رقمی، از جمله این موارد، docker stats --no-stream را روی سرور خود اجرا کنید.

ChartTypical idle container memory, stock images, megabytes
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، 4 و 8 گیگابایت رم جای می‌گیرد

ابتدا سهم سیستم‌عامل را از کل منابع کسر کنید. هسته سیستم‌عامل (kernel)، systemd، journald، sshd و Docker daemon همگی در همان حافظه RAMای قرار دارند که کانتینرهای شما استفاده می‌کنند و dockerd به همراه containerd حدود 100 مگابایت از آن را اشغال می‌کنند. شما همچنین به حافظه آزاد برای page cache و برای لحظات اوج مصرف هنگام build کردن image یا dump گرفتن از دیتابیس نیاز دارید.

ChartRAM budget by plan size, megabytes
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 و فضای خالی (headroom) است که باعث می‌شود سرور تحت بار کاری همچنان پاسخگو باقی بماند. آنچه باقی می‌ماند container_mb است و این تنها عددی است که می‌توانید برای سرویس‌های خود هزینه کنید. این مقدار رزرو شده با بزرگ‌تر شدن پلن افزایش می‌یابد؛ از 768 مگابایت در کوچک‌ترین سرور تا 1536 مگابایت در بزرگ‌ترین سرور، زیرا سرور بزرگ‌تر کانتینرهای بیشتری اجرا می‌کند، لاگ‌های بیشتری می‌نویسد و به page cache بیشتری نیاز دارد.

یک پلن 2 گیگابایتی، 1280 مگابایت برای کانتینرها باقی می‌گذارد. اگر 512 مگابایت از آن را صرف PostgreSQL و 128 مگابایت را صرف Traefik کنید، نیمی از آن تمام شده است. مابقی آن برای دو برنامه کوچک با حدود 256 مگابایت رم کافی است. این یک سرور واقعی و کاربردی است، اما فضایی برای Nextcloud و یک کلاستر جستجو در کنار آن ندارد.

یک پلن 4 گیگابایتی، 3072 مگابایت فضا می‌دهد که در آن یک دیتابیس، یک reverse proxy، سه برنامه و یک کانتینر مانیتورینگ همگی با هم جای می‌گیرند. این کوچک‌ترین اندازه‌ای است که برای کارهای جدی ارزش استفاده دارد، زیرا حافظه اضافی همان چیزی است که خطاهای احتمالی در deploy را جذب می‌کند.

یک پلن 8 گیگابایتی، 6656 مگابایت از کل 8192 مگابایت خود را آزاد می‌گذارد و در این سطح، محدودیت معمولاً از حافظه به سمت CPU یا پهنای باند دیسک تغییر می‌کند. یک دسته از کانتینرها هستند که اندازه خود را بر اساس پیکربندی تعیین می‌کنند نه بار کاری: یک سرور مدل محلی (مانند Ollama)، حافظه KV cache را متناسب با context window خود رزرو می‌کند، بنابراین افزایش num_ctx در Ollama می‌تواند قبل از رسیدن حتی یک درخواست، گیگابایت‌ها از بودجه رم شما را مصرف کند. اگر محاسبات نشان می‌دهد که 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 به هسته فضایی می‌دهد تا صفحات حافظه غیرفعال (cold pages) را به آن منتقل کند؛ این کار به شما چند دقیقه زمان می‌خرد تا متوجه مشکل شوید.

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 خود می‌نویسد. بسته‌ای که به مقصد یک کانتینر ارسال می‌شود، به‌جای تحویل به میزبان (host)، به آن کانتینر فوروارد می‌شود؛ بنابراین در مسیر FORWARD پردازش شده و هرگز از قوانین INPUT که UFW می‌نویسد، عبور نمی‌کند.

شما می‌توانید این فرآیند را روی سرور مشاهده کنید:

sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432

UFW ممکن است وضعیت 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 در زمان ایجاد کانتینر تعیین می‌شود. دستور docker compose up -d را اجرا کنید تا کانتینر دوباره ایجاد شود، سپس مقدار فعال آن را بررسی کنید:

docker inspect my-app | grep -A3 RestartPolicy

سپس سرور را عمداً reboot کنید و در دایرکتوری پروژه دستور docker compose ps را اجرا کنید. stackای که از یک reboot برنامه‌ریزی‌شده جان سالم به در ببرد، در برابر rebootهای ناخواسته نیز مقاوم است. اگر stack شما به ترتیب خاصی برای اجرا یا اجرای یک دستور یک‌باره (one-shot) در زمان بوت نیاز دارد، یک unit در systemd ابزار بهتری است: شروع Docker Compose در زمان بوت شامل فایل unit مربوطه است. برای اینکه بدانید آیا کانتینری که بازگشته واقعاً در حال سرویس‌دهی است، از healthcheck در Compose استفاده کنید.

چرا دیسک VPS من پر شده است؟

دلیل این است که Docker همه چیز را تا زمانی که دستور حذف ندهید، نگه می‌دارد. هر تگ image که تا به حال pull کرده‌اید، هر container متوقف‌شده، هر volume بی‌نامی که پس از بازسازی (recreate) باقی مانده و هر لایه از build cache روی دیسک می‌ماند. در یک فایل‌سیستم root با حجم 40 GB یا 80 GB که در این پلن‌ها معمول است، این موضوع باعث می‌شود که فضای دیسک در عرض چند ماه، نه چند سال، تمام شود.

پر شدن دیسک مانند کرش کردن سیستم نیست. در یک ساعت مشابه، خطای 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 دیتابیس آن نیز همراه با آن حذف می‌شود. پیش از تایپ این فلگ، 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) تعیین کنید، حتی برای سرویس‌هایی که مطمئن هستید سبک هستند. این محدودیت باعث می‌شود یک قطعی در سطح کل میزبان، به ری‌استارت شدن تنها یک کانتینر محدود شود.
  • وضعیت سرور را از جایی دیگر مانیتور کنید تا پیش از آنکه kernel وارد عمل شود، از فشار روی حافظه یا دیسک مطلع شوید. Uptime Kuma در یک کانتینر اجرا می‌شود و در حالت idle حدود 95 مگابایت حافظه مصرف می‌کند.
  • از volumeها نسخه پشتیبان تهیه کنید، نه از کانتینرها. کانتینر دورریختنی است اما volume خیر. پشتیبان‌گیری restic روی یک VPS شامل زمان‌بندی و تست بازیابی است.
  • تگ‌های image را در فایل compose ثابت (pin) کنید و آن‌ها را در روزی که خودتان انتخاب می‌کنید به‌روزرسانی کنید. با استفاده از latest، نسخه‌ای که در docker compose pull بعدی دریافت می‌کنید، همان نسخه‌ای خواهد بود که در آن صبح منتشر شده است.

یک VPS کوچک که Docker روی آن اجرا می‌شود، در صورتی که چهار پارامتر در محدوده مجاز باقی بمانند، سال‌ها سالم می‌ماند: بودجه حافظه، لیست پورت‌های منتشر شده، سیاست ری‌استارت (restart policy) برای هر سرویس، و فضای خالی دیسک. باقی موارد همان 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.