SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

اجرای دیتابیس در Docker یا روی host؟

اجرای PostgreSQL، MySQL یا Redis در Docker برای محیط production کاملاً استاندارد است. چالش اصلی مدیریت صحیح Volumes، ارتقای نسخه، تهیه Backup و محدودیت‌های Memory است.

آیا دیتابیس باید در Docker اجرا شود یا روی host؟

دیتابیس را در Docker اجرا کنید. برای یک پشتهٔ نرم‌افزاری روی یک VPS، استفاده از PostgreSQL، MySQL، MongoDB یا Redis به صورت containerized یک انتخاب استاندارد در محیط production است و بحث‌هایی که معمولاً پیرامون آن مطرح می‌شود، اغلب به بیراهه می‌روند. یک container در واقع یک پردازش لینوکسی است که با namespaces و cgroups محدود شده است، نه یک ماشین مجازی؛ بنابراین هیچ hypervisorای بین دیتابیس و دیسک وجود ندارد. با استفاده از bind mount یا یک local named volume، عملیات خواندن و نوشتن مستقیماً روی فایل‌سیستم host انجام می‌شود، یعنی همان‌جایی که یک نصب معمولی (package install) از آن استفاده می‌کرد.

هزینهٔ واقعی، عملیاتی است. چهار عامل تعیین می‌کنند که این پیکربندی مناسب است یا به یک فاجعهٔ کند تبدیل می‌شود: محل ذخیره‌سازی داده‌ها، مالکیت آن دایرکتوری، نحوهٔ انجام ارتقای نسخهٔ اصلی (major version upgrade) و اینکه آیا تا به حال یک نسخه پشتیبان (backup) را بازیابی کرده‌اید یا خیر. اگر این موارد را به درستی مدیریت کنید، container تنها یک جزئیات فنی خواهد بود. اگر آن‌ها را اشتباه انجام دهید، container همان چیزی است که مقصر اصلی مشکلات خود می‌دانید.

این تصمیم برای تمام دیتابیس‌های سرور یکسان است. مثال‌های زیر از PostgreSQL، MySQL، MongoDB و Redis استفاده می‌کنند و تفاوت‌های مختص به هر محصول در جایی که اهمیت دارند، ذکر شده‌اند.

کانتینر واقعاً چه چیزی را تغییر می‌دهد

تا زمانی که یک مسیر ذخیره‌سازی را mount می‌کنید، مسیر ذخیره‌سازی تغییر نمی‌کند. هسته (kernel)، حافظه کش صفحه (page cache) و سیستم فایل همان باقی می‌مانند.

یک دام عملکردی واقعی وجود دارد و آن زمانی است که هیچ‌چیز را mount نمی‌کنید. بدون volume، دایرکتوری داده‌ها در لایه قابل‌نوشتن کانتینر قرار می‌گیرد که یک سیستم فایل overlay است که روی image قرار گرفته است. نوشتن در این لایه کندتر است و با حذف کانتینر، کل لایه پاک می‌شود. دلیل اینکه «پایگاه داده من صبح امروز خالی بود» همین است.

آنچه واقعاً تغییر می‌کند:

  • چرخه حیات. docker compose down کانتینر را از بین می‌برد. هر چیزی که در یک volume نباشد، همراه با آن از بین می‌رود.
  • نسخه. تگ image همان نسخه است. هیچ apt upgrade درون یک کانتینر پایگاه داده وجود ندارد که از docker compose pull بعدی جان سالم به در ببرد.
  • حسابرسی حافظه. محدودیت cgroup یک دیوار سخت است که توسط هسته اعمال می‌شود و پایگاه داده از وجود آن بی‌اطلاع است.
  • کاربر. پردازش به عنوان یک شناسه کاربری عددی (numeric user id) درون کانتینر اجرا می‌شود که ممکن است مالک هیچ‌چیزی روی host شما نباشد.

محل ذخیره‌سازی داده‌ها تعیین‌کننده همه چیز است

دو گزینه مناسب و یک اشتباه رایج وجود دارد.

  • یک volume نام‌گذاری‌شده: pgdata:/var/lib/postgresql/data. داکر دایرکتوری را در /var/lib/docker/volumes/<project>_pgdata/_data ایجاد می‌کند و entrypoint ایمیج در اولین اجرا، مالکیت (ownership) آن را تنظیم می‌کند. این گزینه پیش‌فرض است.
  • یک bind mount: /srv/appname/pg:/var/lib/postgresql/data. شما مسیر را انتخاب می‌کنید، بنابراین مسئولیت مشکلات مجوزها (permissions) با شماست.
  • عدم استفاده از mount. به مورد بالا مراجعه کنید. داده‌ها درون کانتینر باقی می‌مانند.

بررسی کامل مزایا و معایب این روش‌ها موضوعی مفصل است و مقایسه bind mounts با named volumes آن را پوشش می‌دهد. برای یک دیتابیس، خلاصه مطلب این است: از یک volume نام‌گذاری‌شده استفاده کنید مگر اینکه دلیل خاصی برای دانستن مسیر میزبان (host path) داشته باشید. اگر از bind mount استفاده می‌کنید، آن را در مکانی پایدار مانند /srv/appname/pg قرار دهید، نه داخل دایرکتوری پروژه که ممکن است توسط git clean در دسترس باشد.

یک محدودیت جدی: دایرکتوری داده‌های دیتابیس را روی NFS (سیستم فایل شبکه) یا هر mount شبکه‌ای که رفتار قفل‌گذاری (locking) و fsync آن را تست نکرده‌اید، قرار ندهید. دیتابیس‌ها فرض می‌کنند که یک fsync موفق به معنای قرار گرفتن بایت‌ها روی حافظه پایدار (stable storage) است. وقتی این فرض اشتباه باشد، با خرابی داده‌هایی مواجه می‌شوید که ممکن است هفته‌ها بعد خود را نشان دهد.

نام‌گذاری ثابت برای volume پیش از ناپدید شدن آن

ابزار Compose برای volumeها نامی به فرمت <project>_<volume> انتخاب می‌کند و نام پروژه به‌صورت پیش‌فرض همان نام دایرکتوری است. بنابراین هویت volume به نام دایرکتوری وابسته است؛ چیزی که کاربران بدون تأمل آن را تغییر می‌دهند.

اگر /srv/app را به /srv/app-old منتقل کنید یا کلید pgdata را در فایل compose تغییر دهید، دستور docker compose up -d بعدی یک volume جدید و خالی ایجاد می‌کند. Postgres در آن یک کلاستر تازه مقداردهی اولیه می‌کند. کانتینر سالم است، برنامه اجرا می‌شود، اما تمام جداول ناپدید شده‌اند. خبر خوب این است که volume قدیمی همچنان با نام قبلی روی دیسک باقی مانده است.

docker volume ls
docker volume inspect app_pgdata

نام‌ها را ثابت (Pin) کنید تا چنین اتفاقی رخ ندهد. نام پروژه و نام volume را به‌صورت صریح تعیین کنید:

name: myapp

services:
  db:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    name: myapp_pgdata

اگر داده‌های شما در یک volume سرگردان قرار دارد، پس از متوقف کردن دیتابیس، آن را کپی کنید:

docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start db

اگر در حین اجرای دیتابیس اقدام به کپی کنید، با یک کپی ناقص (torn copy) از فایل‌هایی که در حال نوشتن بوده‌اند مواجه می‌شوید. ابتدا آن را متوقف کنید.

چه کسی مالک دایرکتوری داده است

ایمیج‌های رسمی Postgres، MySQL و MongoDB سرور خود را با یک user id فاقد امتیاز، معمولاً 999، اجرا می‌کنند. هنگامی که کانتینر به عنوان root شروع به کار می‌کند، entrypoint مالکیت دایرکتوری داده را به آن کاربر تغییر می‌دهد و سپس امتیازات را رها می‌کند. به همین دلیل است که یک bind mount خالی معمولاً در اولین تلاش به درستی کار می‌کند.

به محض اینکه user: را در فایل compose تنظیم کنید، این فرآیند مختل می‌شود، زیرا در آن حالت entrypoint دیگر امتیازی برای اصلاح هیچ‌چیزی ندارد. Postgres این موضوع را مستقیماً اعلام می‌کند:

initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permitted

یک دایرکتوری داده که با mode اشتباه وجود داشته باشد، پیام متفاوتی می‌دهد و شناختن این پیام ارزشمند است، زیرا راه حل آن chmod است، نه chown:

FATAL:  data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL:  Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).

MongoDB روی یک bind mount که مالک آن root است، در مرحله فایل قفل (lock file) با شکست مواجه می‌شود:

Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.

راه حل این است که مالکیت دایرکتوری میزبان (host) را با استفاده از شناسه عددی تغییر دهید، نه با نام:

sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pg

دستور ls -ldn اعداد را به جای نام‌ها چاپ می‌کند و باید 999 999 را نشان دهد. حسابی که روی میزبان شما postgres نامیده می‌شود و حسابی که داخل ایمیج postgres نام دارد، هیچ ارتباطی به هم ندارند: هسته سیستم‌عامل اعداد را مقایسه می‌کند و نام‌ها در هر سمت به‌طور جداگانه جستجو می‌شوند. نحوه نگاشت کاربران میزبان به کانتینر توسط PUID و PGID این نگاشت را به‌طور کامل بررسی می‌کند. در حالت rootless Docker یا user namespace remapping، این اعداد دوباره تغییر می‌کنند، بنابراین به جای فرض کردن عدد 999، شناسه‌ها را از کانتینر در حال اجرا بخوانید.

استفاده از Named volumes باعث می‌شود کل این بخش در اولین اجرا حذف شود، زیرا Docker یک دایرکتوری خالی ایجاد می‌کند و entrypoint مالکیت آن را در اختیار می‌گیرد.

ارتقا: ارتقای بسته در برابر تغییر تگ ایمیج

روی میزبان، apt upgrade شما را در یک نسخه فرعی جابه‌جا می‌کند. توزیع شما به‌طور خودکار نسخه اصلی (major) دیتابیس را ارتقا نمی‌دهد و زمانی که تصمیم به ارتقا می‌گیرید، هر دو مجموعه از باینری‌ها می‌توانند هم‌زمان نصب باشند، که این دقیقاً همان چیزی است که pg_upgrade به آن نیاز دارد.

در یک کانتینر، تگ همان نسخه است، بنابراین ارتقا به معنای ویرایش یک خط است. این موضوع ارتقاهای فرعی را ساده و ارتقاهای اصلی را به یک رویه تبدیل می‌کند.

مقدار postgres:16 را به postgres:17 تغییر دهید، دستور docker compose up -d را اجرا کنید؛ کانتینر بلافاصله خارج می‌شود:

PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.

هیچ‌چیز آسیب نمی‌بیند. باینری‌های جدید از خواندن ساختار کاتالوگ قدیمی روی دیسک که بین نسخه‌های اصلی تغییر می‌کند، خودداری می‌کنند. تگ را به postgres:16 برگردانید تا دوباره شروع به کار کند. این بازگشت به نسخه قبل (rollback)، تنها مزیت واقعی ارتقا در کانتینرها است.

مسیر پشتیبانی‌شده، dump و restore است. PostgreSQL ترجیح می‌دهد dump توسط کلاینت جدیدتر گرفته شود، بنابراین آن را از داخل ایمیج جدید و علیه سرور قدیمی که هنوز روی شبکه compose در حال اجراست، اجرا کنید:

docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
  postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sql

حجم فایل باید حداقل ده‌ها کیلوبایت باشد و با خطی که حاوی PostgreSQL database cluster dump complete است پایان یابد. فایلی با حجم چند صد بایت به این معنی است که dump با شکست مواجه شده و شما در آستانه حذف بیهوده یک volume هستید. تنها پس از این بررسی:

docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sql

سایر موتورها متفاوت عمل می‌کنند:

  • MySQL 8 دیکشنری داده‌های خود را در زمان شروع به کار ارتقا می‌دهد، بنابراین تغییر تگ فرعی معمولاً فقط یک restart است. پیش از پرش بین سری‌های انتشار، یادداشت‌های انتشار (release notes) را بخوانید و در هر صورت ابتدا یک dump بگیرید.
  • MariaDB انتظار دارد که پس از بالا آمدن سرور روی نسخه جدید، دستور mariadb-upgrade اجرا شود.
  • MongoDB باید هر بار یک نسخه اصلی ارتقا یابد و پس از هر مرحله، پیش از ادامه کار، باید نسخه سازگاری ویژگی (feature compatibility version) را تنظیم کنید. پرش از یک نسخه باعث می‌شود mongod از شروع کار خودداری کند و در لاگ‌ها خط UPGRADE PROBLEM را با نام featureCompatibilityVersion ثبت کند. از نسخه 7.0 به بعد، این دستور به یک فلگ تأیید صریح نیاز دارد: db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true }).
  • Redis فایل‌های snapshot قدیمی‌تر را به‌راحتی بارگذاری می‌کند اما فایل‌های جدیدتر را خیر؛ بنابراین ارتقا یک restart است و بازگشت به نسخه قبل (downgrade) ممکن است در بارگذاری داده‌ها با شکست مواجه شود.

قانون کلی: کانتینر بازگشت به نسخه قبل را آسان می‌کند اما ارتقا را آسان‌تر نمی‌کند.

چرا کانتینر دیتابیس من با کد 137 متوقف می‌شود؟

دلیل آن این است که OOM killer (قاتل کمبود حافظه) هسته سیستم‌عامل، آن را متوقف کرده است. کد 137 حاصل جمع 128 و سیگنال 9 است.

docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20

docker compose ps مقدار Exited (137) را نشان می‌دهد، خط inspect شامل "OOMKilled": true است و لاگ هسته نیز ورودی مشابهی دارد:

Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kB

مکانیسم این اتفاق برای بسیاری غافلگیرکننده است. PostgreSQL و MySQL اندازه بافرهای خود را بر اساس کل حافظه گزارش‌شده توسط میزبان تعیین می‌کنند. محدودیت cgroup این عدد را برای آن‌ها تغییر نمی‌دهد. در یک میزبان 16 گیگابایتی با محدودیت 2 گیگابایت، دیتابیس طوری برنامه‌ریزی می‌کند که انگار 16 گیگابایت حافظه دارد و cgroup مدت‌ها پیش از آنکه میزبان تحت فشار قرار بگیرد، آن را متوقف می‌کند. بنابراین، تعیین محدودیت حافظه به‌تنهایی کافی نیست. شما باید به دیتابیس بگویید که چه مقدار حافظه در اختیار دارد:

  • PostgreSQL: مقدار shared_buffers را تنظیم کنید و به work_mem توجه داشته باشید. work_mem به ازای هر عملیات مرتب‌سازی در هر اتصال تخصیص می‌یابد؛ بنابراین یک مقدار سخاوتمندانه ضرب‌در پنجاه اتصال، دلیل معمول متوقف شدن کانتینر تحت بار (به‌جای زمان راه‌اندازی) است.
  • MySQL و MariaDB: مقدار innodb_buffer_pool_size را تنظیم کنید که به‌صورت پیش‌فرض 128M است. در کانتینر، innodb_dedicated_server را غیرفعال کنید، زیرا وظیفه اصلی آن تنظیم خودکار بر اساس حافظه شناسایی‌شدهٔ ماشین است.
  • MongoDB: اندازه کش WiredTiger را به‌جای اجازه دادن به آن برای حدس زدن از روی حافظه میزبان، به‌صورت صریح تعیین کنید.
  • Redis: مقدار maxmemory به‌صورت پیش‌فرض نامحدود است، بنابراین Redis تا زمانی که cgroup آن را متوقف کند، رشد می‌کند. مقدار maxmemory را به‌اندازه کافی پایین‌تر از محدودیت کانتینر تنظیم کنید و یک maxmemory-policy مناسب انتخاب کنید.

Postgres نیز این رویداد را از سمت خود گزارش می‌دهد و این جفت خط همان چیزی است که در لاگ خواهید یافت:

LOG:  server process (PID 123) was terminated by signal 9: Killed
LOG:  terminating any other active sessions due to crash of another server process

متوقف شدن یک backend باعث می‌شود تمام backendهای دیگر نیز ری‌استارت شوند، زیرا حافظه اشتراکی ممکن است اکنون ناسازگار باشد. این یک طوفان اتصال برای برنامه شماست، نه یک رویداد بی‌سروصدا. تنظیم محدودیت حافظه در Docker Compose سینتکس و تفاوت بین mem_limit و فرم deploy.resources را پوشش می‌دهد.

هیچ‌کدام از این مشکلات با حذف محدودیت روی میزبان از بین نمی‌روند، بلکه فقط جابه‌جا می‌شوند. بدون cgroup، دیتابیس با سایر سرویس‌های روی سیستم رقابت می‌کند و OOM killer میزبان، قربانی را بر اساس امتیاز انتخاب می‌کند که ممکن است حتی sshd باشد. محدودیتی که دیتابیس را به‌صورت قابل‌پیش‌بینی متوقف می‌کند، مدیریت آسان‌تری نسبت به OOM میزبان دارد که ممکن است دسترسی شما را به‌کلی مسدود کند.

پشتیبان‌گیری: تهیه dump در داخل، انتقال به خارج

هرگز از یک دیتابیس در حال اجرا با کپی کردن دایرکتوری داده‌های آن پشتیبان نگیرید. کپی در سطح فایل که هنگام نوشتن سرور انجام شود، یک کپی ناقص (torn copy) است و شما این موضوع را تنها در زمان بازیابی متوجه خواهید شد.

دو روش مطمئن وجود دارد: یا با استفاده از ابزار اختصاصی دیتابیس در حین اجرا یک dump تهیه کنید و از آن پشتیبان بگیرید، یا کانتینر را متوقف کرده و volume را به صورت cold کپی کنید.

docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVE

استفاده از -T اهمیت دارد. بدون آن، docker compose exec ممکن است یک ترمینال به دستور متصل کند و لایه ترمینال کاراکترهای بازگشت به ابتدای خط (carriage return) را به جریان خروجی اضافه می‌کند. در نتیجه، یک dump متنی با خطاهای عجیب بازیابی می‌شود و یک dump باینری به سادگی خراب خواهد بود. این مشکل در زمان پشتیبان‌گیری بی‌صدا رخ می‌دهد و یک ماه بعد با صدای بلند (هنگام نیاز به بازیابی) خود را نشان می‌دهد.

--single-transaction به mysqldump یک snapshot منسجم از جداول InnoDB می‌دهد بدون اینکه کل سرور را قفل کند.

این دستورات هر کدام تنها یک فایل تولید می‌کنند. این‌ها یک سیستم پشتیبان‌گیری نیستند: نه سیاست نگهداری (retention) دارند، نه کپی خارج از سرور و نه فرآیند تایید سلامت. دایرکتوری dump را به ابزاری بسپارید که هر سه مورد را انجام دهد، که این دقیقاً همان کاری است که پشتیبان‌گیری با restic از یک VPS برای آن طراحی شده است. از /srv/backups پشتیبان بگیرید، نه از /var/lib/docker/volumes.

سپس فرآیند بازیابی را تست کنید، زیرا پشتیبانی که هرگز بازیابی نشده است، در واقع پشتیبان نیست:

docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'

\dt باید جداول برنامه شما را لیست کند. نتیجه خالی یا Did not find any relations. به این معنی است که dump آن چیزی نیست که تصور می‌کنید. پس از اتمام کار، restore_test را حذف کنید.

دستوری که همه چیز را حذف می‌کند

docker compose down -v.

دستور ساده down کانتینرها و شبکه را حذف می‌کند. دستور -v علاوه بر آن‌ها، تمام volumeهای نام‌گذاری‌شده‌ای که در آن فایل compose تعریف شده‌اند و همچنین تمام volumeهای بی‌نام متصل به آن کانتینرها را نیز پاک می‌کند. این دستور هیچ تأییدیه‌ای نمی‌گیرد و قابل بازگشت نیست. این رایج‌ترین روشی است که دیتابیس‌های self-hosted از بین می‌روند و معمولاً هنگام عیب‌یابی یک مشکل بی‌ربط اتفاق می‌افتد، چون در پاسخ یک انجمن گفته شده که آن را اجرا کنید.

چهار مورد زیر شعاع تخریب را کاهش می‌دهند:

  • دیتابیس volume را به صورت external: true تعریف کنید. Compose نمی‌تواند volumeای را که مالک آن نیست حذف کند، بنابراین -v نمی‌تواند به آن دسترسی پیدا کند. شما آن را یک بار با docker volume create myapp_pgdata ایجاد می‌کنید.
  • برای ری‌استارت‌های روتین از docker compose stop و docker compose start استفاده کنید. تفاوت down و stop در Compose توضیح می‌دهد که هر کدام چه چیزی را حذف می‌کنند.
  • فایل‌های dump را در یک مسیر روی host، خارج از هر volume که توسط compose مدیریت می‌شود، نگه دارید.
  • هرگز -v را از پاسخ‌های عیب‌یابی در استکی که حاوی داده‌های مهم شماست، کپی و اجرا نکنید.

پورت دیتابیس را منتشر نکنید

این خط دیتابیس شما را روی اینترنت عمومی قرار می‌دهد:

    ports:
      - "5432:5432"

این دستور باعث می‌شود سرویس روی تمام اینترفیس‌ها گوش دهد. Docker با بازنویسی مقصد بسته‌ها پیش از آنکه قوانین ورودی فایروال شما آن‌ها را ببینند، پورت را منتشر می‌کند؛ از آنجا که قوانین ufw در زنجیره input قرار دارند، ufw deny 5432 عملاً هیچ کاری انجام نمی‌دهد. چرا پورت‌های منتشر شده توسط Docker از ufw عبور می‌کنند نحوه پیمایش زنجیره‌ها را نشان می‌دهد.

یک برنامه در همان پروژه compose از طریق نام سرویس در شبکه compose به دیتابیس دسترسی پیدا می‌کند، بنابراین نیازی به پورت منتشر شده ندارد. این بلوک را حذف کنید. اگر به کلاینتی روی همان میزبان نیاز دارید، آن را فقط به loopback متصل کنید:

    ports:
      - "127.0.0.1:5432:5432"

بررسی کنید چه چیزی در حال حاضر در حال گوش دادن است:

sudo ss -ltnp | grep 5432

127.0.0.1:5432 همان چیزی است که باید باشد. 0.0.0.0:5432 به این معنی است که هر کسی می‌تواند رمز عبور شما را امتحان کند.

چه چیزی را کجا اجرا کنیم

یک برنامه روی یک VPS. از Container استفاده کنید. یک volume با نام مشخص و ثابت تعریف کنید، پورت را publish نکنید، محدودیت حافظه (memory limit) با تنظیمات متناسب دیتابیس اعمال کنید و یک dump شبانه به مسیری در host بگیرید که restic آن را جمع‌آوری کند. از یک نصب تمیز Docker روی VPS شروع کنید و کل stack را در یک فایل compose نگه دارید که آن را commit می‌کنید. مزیت این کار واقعی است: نسخهٔ دیتابیس به یک خط قابل‌بررسی در git تبدیل می‌شود.

یک host که چندین سرویس را اجرا می‌کند. از Container استفاده کنید و برای هر برنامه یک دیتابیس جداگانه در نظر بگیرید، نه یک دیتابیس مشترک برای همه. استفاده از یک سرور مشترک، تمام برنامه‌ها را به یک برنامهٔ زمان‌بندی ارتقا وابسته می‌کند و یک کوئری سنگین می‌تواند باعث از دسترس خارج شدن همهٔ سرویس‌ها شود. برای هر container محدودیت حافظه تعیین کنید تا یک کوئری مخرب فقط به همان برنامه‌ای محدود شود که آن را ایجاد کرده است. چندین نمونهٔ کوچک Postgres فضای دیسک کمی بیشتر اشغال می‌کنند اما هماهنگی بسیار کمتری نیاز دارند.

دیتابیس، خودِ محصول است. آن را مستقیماً روی host از طریق مخزن بسته‌های (package repository) سازنده اجرا کنید یا هزینهٔ یک سرویس مدیریت‌شده (managed service) را بپردازید. pg_upgrade به نصب همزمان دو نسخهٔ اصلی از باینری‌ها نیاز دارد که بسته‌های سیستمی این امکان را به شما می‌دهند، اما imageهای تک‌نسخه‌ای خیر. Replication و بازیابی در لحظه (point in time recovery) با استفاده از آرشیو کردن WAL (write ahead log)، زمانی که دیتابیس مالک ماشین و دیسک‌های آن باشد، بسیار ساده‌تر است. برای سیستمی که قرار است ساعت 03:00 صبح شما را با هشدار بیدار کند، مسیر مطمئن و ساده را انتخاب کنید.

برنامه کوچک است. اصلاً به فکر اجرای دیتابیس سروری نباشید. برای یک برنامهٔ وب با یک نویسنده روی یک VPS، استفاده از SQLite در محیط production روی VPS اغلب گزینهٔ بهتری است؛ جایی که پشتیبان‌گیری فقط شامل یک فایل است و مسیر ارتقا، صرفاً به‌روزرسانی نسخهٔ کتابخانه است.

FAQ

آیا اجرای دیتابیس در محیط production با Docker ایمن است؟

بله، برای پشته‌های نرم‌افزاری تک‌سرور. کانتینر در واقع یک پردازش لینوکسی است که توسط namespaces و cgroups محدود شده است؛ بنابراین با mount کردن یک volume، دیتابیس دقیقاً روی همان فایل‌سیستمی می‌نویسد که در صورت نصب مستقیم از پکیج استفاده می‌کرد. ریسک‌ها بیشتر عملیاتی هستند تا مربوط به سرعت: volumeای که نام آن ثابت (pin) نشده باشد، bind mountای که مالکیت آن با user id اشتباه است، restoreای که هرگز تست نکرده‌اید و docker compose down -v. اگر این چهار مورد را مدیریت کنید، کانتینر مشکلی نخواهد داشت. زمانی که دیتابیس بار کاری اصلی شماست و به pg_upgrade، replication یا point in time recovery نیاز دارید، به نصب مستقیم روی host مهاجرت کنید.

برای داده‌های دیتابیس از bind mount استفاده کنم یا named volume؟

مگر اینکه دلیل خاصی برای دانستن مسیر دقیق روی host داشته باشید، از named volume استفاده کنید. Docker دایرکتوری را می‌سازد و entrypoint ایمیج در اولین اجرا مالکیت آن را تنظیم می‌کند، بنابراین مشکل مجوزها هرگز پیش نمی‌آید. volume را با یک name: صریح ثابت کنید یا آن را external: true علامت بزنید؛ در غیر این صورت، تغییر نام دایرکتوری پروژه به‌طور خودکار یک volume خالی جدید و یک دیتابیس خالی ایجاد می‌کند. اگر مالکیت دایرکتوری host را با chown به numeric user id که ایمیج با آن اجرا می‌شود تغییر دهید، bind mount نیز مناسب است؛ این مقدار برای ایمیج‌های رسمی Postgres، MySQL و MongoDB برابر با 999 است. آن را با ls -ldn بررسی کنید، زیرا ls -l نام کاربری host شما را برای آن عدد نشان می‌دهد و آن نام در داخل کانتینر بی‌معنی است.

دستور docker compose down -v چه چیزی را حذف می‌کند؟

این دستور کانتینرها و شبکه را مانند یک down ساده حذف می‌کند، و -v علاوه بر آن، تمام named volumeهای تعریف‌شده در فایل compose و تمام anonymous volumeهای متصل به آن کانتینرها را نیز پاک می‌کند. این شامل دیتابیس هم می‌شود. هیچ پیام تأییدی وجود ندارد و امکان بازیابی نیست. volumeهایی که external: true علامت‌گذاری شده‌اند حذف نمی‌شوند، که این اصلی‌ترین دلیل برای external کردن volume دیتابیس است. برای راه‌اندازی مجدد معمول، از docker compose stop و docker compose start استفاده کنید.

چگونه PostgreSQL را در Docker به نسخه اصلی (major) جدید ارتقا دهم؟

از روش dump و restore استفاده کنید. تغییر postgres:16 به postgres:17 و راه‌اندازی مجدد، منجر به FATAL: database files are incompatible with server با یک خط DETAIL می‌شود که هر دو نسخه را نام می‌برد، زیرا باینری‌های جدید نمی‌توانند ساختار کاتالوگ قدیمی را بخوانند. هیچ آسیبی به داده‌ها نمی‌رسد: تگ قدیمی را برگردانید تا سرویس بالا بیاید. با استفاده از کلاینت نسخه جدید علیه کانتینر قدیمی در حال اجرا، یک pg_dumpall بگیرید، مطمئن شوید فایل با PostgreSQL database cluster dump complete تمام می‌شود، سپس تگ جدید را روی یک volume خالی بالا بیاورید و dump را load کنید. ارتقاهای جزئی (minor) در یک نسخه اصلی، تنها به pull و restart نیاز دارند.

چرا کانتینر دیتابیس من با کد 137 خارج می‌شود؟

137 حاصل 128 به علاوه سیگنال 9 است، یعنی چیزی پردازش را مستقیماً کشته است. دستور docker inspect <container> | grep -i oomkilled را اجرا کنید؛ مقدار true به این معنی است که کانتینر به محدودیت حافظه cgroup خود رسیده است. دلیل معمول این است که PostgreSQL و MySQL حافظه کل host را می‌خوانند و محدودیت کانتینر را نمی‌بینند، بنابراین برای 16 گیگابایت برنامه‌ریزی می‌کنند در حالی که در 2 گیگابایت محدود شده‌اند. برای تطبیق با محدودیتی که به کانتینر داده‌اید، shared_buffers و work_mem یا innodb_buffer_pool_size را تنظیم کنید. برای تأیید اینکه هسته کدام پردازش را انتخاب کرده است، journalctl -k را برای خط Memory cgroup out of memory مربوطه بررسی کنید.