SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor

آموزش پشتیبان‌گیری و ارتقای اصولی Docker Compose

برای پشتیبان‌گیری از Docker Compose باید فایل compose، متغیرهای env، حجم‌های داده و خروجی دیتابیس را ذخیره کنید. این راهنما روش صحیح ارتقا و تست سلامت بازگردانی را توضیح می‌دهد.

محتویات لازم برای پشتیبان‌گیری از یک استک Docker Compose

پشتیبان‌گیری از یک استک Docker Compose باید شامل چهار مورد مجزا باشد و از دست دادن هر یک از آن‌ها به معنای عدم بازگشت برنامه به وضعیت قبلی است: فایل compose، فایل .env موجود در کنار آن، محتویات تمامی volumeها، و یک خروجی dump از پایگاه داده که توسط کلاینت اختصاصی همان پایگاه داده تهیه شده باشد. کپی کردن فایل‌های پایگاه داده در حالی که container آن در حال اجرا است، پشتیبان محسوب نمی‌شود. برای ارتقا (upgrade) از همین لیست استفاده کنید، با این تفاوت که یک قانون مهم وجود دارد: پیش از اجرای دستور pull، حتماً پشتیبان تهیه کنید؛ زیرا migrationهای مربوط به schema برای اجرا به سمت جلو طراحی شده‌اند و اکثر پروژه‌ها راهی برای بازگشت (rollback) ارائه نمی‌دهند.

تمام موارد زیر فرض را بر این می‌گذارند که استک از قبل مستقر شده و دستور docker compose ps فعال بودن آن را نشان می‌دهد. مثال‌ها از یک دایرکتوری پروژه در /srv/myapp با سرویس‌هایی به نام app و db استفاده می‌کنند. نام‌های اختصاصی خود را جایگزین کنید. دستورات عمداً به صورت کلی نوشته شده‌اند، زیرا بخش‌های حیاتی یعنی volumeها و پایگاه داده، فارغ از نوع برنامه، به روش یکسانی عمل می‌کنند.

تشخیص دقیق داده‌های ذخیره‌شده در استک

cd /srv/myapp
docker compose ps
docker compose config --volumes
docker volume ls --filter label=com.docker.compose.project=myapp

دستور docker compose config --volumes نام‌های کوتاه volumeهای نام‌گذاری‌شده‌ای که در فایل خود تعریف کرده‌اید را چاپ می‌کند. دستور docker volume ls نام‌هایی که این volumeها در واقع روی دیسک دارند را نمایش می‌دهد. این دو لیست با هم تفاوت دارند، زیرا Compose نام پروژه را به ابتدای نام volume اضافه می‌کند: volumeای که در فایل به صورت db_data نوشته شده، روی دیسک با نام myapp_db_data وجود دارد. نام پروژه به‌طور پیش‌فرض همان نام دایرکتوری است؛ بنابراین تغییر نام دایرکتوری باعث می‌شود استک به مجموعه‌ای جدید و خالی از volumeها اشاره کند و volumeهای قدیمی با تمام داده‌های شما در جای خود باقی بمانند. تمام دستورات زیر به نام واقعی از خروجی docker volume ls نیاز دارند.

Bind mountها در هیچ‌کدام از این لیست‌ها ظاهر نمی‌شوند. در فایل compose، این موارد ورودی‌هایی هستند که در سمت چپ علامت دو نقطه، یک مسیر در host دارند، مانند ./config:/app/config. این‌ها دایرکتوری‌های معمولی در host هستند، بنابراین ابزارهای استاندارد به آن‌ها دسترسی دارند. Named volumeها در مسیر /var/lib/docker/volumes/ قرار دارند و دستور docker volume inspect --format '{{.Mountpoint}}' myapp_db_data مسیر دقیق یکی از آن‌ها را چاپ می‌کند. نوع استفادهٔ استک شما از این موارد، نحوهٔ کپی کردن آن‌ها را تغییر می‌دهد و تفاوت bind mountها با named volumeها این مبحث را به‌طور کامل پوشش می‌دهد.

اکنون مواردی که پیدا کرده‌اید را به دو گروه تقسیم کنید. برخی از volumeها حاوی وضعیتی (state) هستند که هیچ‌چیز نمی‌تواند آن را بازسازی کند: فایل‌های آپلود شده، کلیدهای تولید شده، خودِ دیتابیس و هر چیزی که کاربر در برنامه وارد کرده است. گروه دیگر حاوی داده‌های مشتق‌شده مانند تصاویر بندانگشتی (thumbnails) و ایندکس‌های جستجو هستند که برنامه می‌تواند به‌طور خودکار آن‌ها را بازسازی کند. بک‌آپ گرفتن از گروه دوم فقط فضای دیسک و زمان بازیابی را هدر می‌دهد و هیچ فایده‌ای ندارد. یک volume برای کش Redis واضح‌ترین مثال است: از دست دادن آن فقط منجر به یک درخواست اولیه کند می‌شود.

پشتیبان‌گیری از فایل compose و فایل .env

هر دو فایل در کنار یکدیگر روی میزبان قرار دارند و هیچ‌کدام درون volumeها نیستند. فایل .env حاوی رمز عبور پایگاه داده، secret برنامه و تمامی توکن‌های API است؛ بنابراین این همان فایلی است که مجموعه‌ای از volumeها را دوباره به یک برنامهٔ عملیاتی تبدیل می‌کند. این فایل معمولاً در .gitignore نیز لیست می‌شود، که به این معنی است که طرح «پیکربندی من در git است» شامل فایلی که بیشترین اهمیت را دارد نمی‌شود. نگهداری secretها در یک فایل env الگوی درستی است و وظیفهٔ مشابهی را برای پشتیبان‌گیری شما ایجاد می‌کند.

sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/backups/myapp
cp -a compose.yaml .env /srv/backups/myapp/
chmod 600 /srv/backups/myapp/.env

از تمام فایل‌های compose که stack از آن‌ها استفاده می‌کند کپی تهیه کنید، نه فقط اولین فایل. stackای که با -f compose.yaml -f compose.prod.yaml راه‌اندازی شده است، برای بازگشت به همان وضعیت به هر دو فایل نیاز دارد و نحوه ادغام چندین فایل compose تعیین می‌کند که کدام مقادیر واقعاً به container می‌رسند.

یک هشدار، .env را به volumeها مرتبط می‌کند. image رسمی Postgres تنها زمانی POSTGRES_PASSWORD را می‌خواند که یک دایرکتوری دادهٔ خالی را مقداردهی اولیه (initialize) کند. تغییر این مقدار در زمان‌های بعدی، رمز عبور داخل پایگاه داده را تغییر نمی‌دهد. اگر volume ماه گذشته را در کنار .env امروز بازیابی کنید، برنامه با خطای FATAL: password authentication failed for user "appuser" در اتصال مواجه می‌شود، در حالی که هر دو فایل در بررسی ظاهری درست به نظر می‌رسند. همیشه .env و volumeها را که مربوط به یک لحظهٔ زمانی یکسان هستند، در کنار هم در یک پشتیبان نگهداری کنید.

تهیه dump از پایگاه داده با استفاده از کلاینت اختصاصی آن

سرور پایگاه داده به‌طور مداوم در حال نوشتن روی فایل‌های خود است. یک tar از /var/lib/postgresql/data که در حین اجرای سرور گرفته شود، برخی صفحات را از پیش از نوشتن و برخی را از پس از آن کپی می‌کند؛ بنابراین آرشیو حاصل، ترکیبی از لحظات مختلف است که ممکن است قابل بازیابی نباشد. ابزار dump، داده‌ها را در یک تراکنش واحد می‌خواند، بنابراین فایل خروجی حاوی یک لحظهٔ منسجم و یکپارچه است. این تفاوت، مرز میان یک نسخهٔ پشتیبان (backup) و یک کپی ساده است.

docker compose exec -T db sh -c \
  'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
  > /srv/backups/myapp/db-$(date +%F).dump

فلگ -T را حفظ کنید. این فلگ تخصیص TTY را غیرفعال می‌کند؛ در صورت متصل بودن TTY، داکر جریان خروجی را در مسیر رسیدن به shell شما ترجمه می‌کند که باعث خرابی یک dump باینری می‌شود. شما تا زمانی که عملیات restore با شکست مواجه نشود، متوجه این موضوع نخواهید شد. استفاده از کوتیشن‌های تکی نیز اهمیت دارد: آن‌ها مانع از آن می‌شوند که shell میزبان شما $POSTGRES_USER را گسترش دهد، بنابراین shell داخل کانتینر آن را گسترش می‌دهد و از مقادیری استفاده می‌کند که فایل compose از پیش تنظیم کرده است. -Fc خروجی را در قالب سفارشی می‌نویسد که هم‌زمان فشرده‌سازی می‌شود و به pg_restore اجازه می‌دهد بعداً اشیاء خاصی را از آن استخراج کند.

نقش‌ها (roles) و رمزهای عبور آن‌ها خارج از هر پایگاه دادهٔ تکی قرار دارند، بنابراین از آن‌ها نیز نسخه پشتیبان تهیه کنید:

docker compose exec -T db sh -c 'pg_dumpall -U "$POSTGRES_USER" --globals-only' \
  > /srv/backups/myapp/globals.sql

سپس بررسی کنید که فایل حاصل یک dump است و نه یک پیام خطا:

ls -lh /srv/backups/myapp/
head -c 5 /srv/backups/myapp/db-$(date +%F).dump

یک dump با قالب سفارشی با پنج بایت PGDMP شروع می‌شود. فایلی با حجم صفر بایت، یا فایلی که با pg_dump: شروع شود، به معنای شکست دستور است. shell فایل خروجی را پیش از اجرای دستور ایجاد می‌کند، بنابراین یک dump ناموفق همچنان فایلی با نام و زمان‌بندی معقول باقی می‌گذارد. این رایج‌ترین نوع شکست خاموش در تهیه نسخه پشتیبان است.

برای MariaDB یا MySQL کلاینت تغییر می‌کند اما ساختار دستور خیر:

docker compose exec -T db sh -c \
  'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction --databases "$MARIADB_DATABASE"' \
  > /srv/backups/myapp/db-$(date +%F).sql

--single-transaction یک dump منسجم از جداول InnoDB بدون مسدود کردن نویسنده‌ها ارائه می‌دهد. در image مربوط به MySQL، دستور mysqldump است و متغیرها MYSQL_ROOT_PASSWORD و MYSQL_DATABASE هستند. در imageهای فعلی MariaDB، دستور mysqldump همچنان به عنوان نام سازگار با mariadb-dump کار می‌کند. توجه داشته باشید که رمز عبوری که در خط فرمان وارد می‌شود، تا زمانی که dump در حال اجراست، در لیست پردازش‌های کانتینر قابل مشاهده است.

SQLite نیاز به مراقبت خاص خود دارد. پایگاه داده یک فایل واحد است، اما تراکنش‌های اخیر ممکن است همچنان در یک فایل -wal جداگانه در کنار آن قرار داشته باشند؛ بنابراین کپی کردن تنها فایل .db باعث می‌شود پایگاه داده‌ای داشته باشید که جدیدترین نوشته‌ها را ندارد. اگر image شامل کلاینت باشد، sqlite3 /data/app.db ".backup '/data/app-backup.db'" یک کپی منسجم در حین اجرای برنامه می‌نویسد. اگر کلاینت موجود نیست، کانتینر را متوقف کنید و فایل .db را به همراه فایل‌های همراه آن یعنی -wal و -shm کپی کنید.

اگر پایگاه داده شما به جای داخل stack، روی میزبان (host) اجرا می‌شود، همان دستورات بدون پیشوند docker compose exec اعمال می‌شوند و مطالعه اجرای پایگاه داده در داکر یا روی میزبان پیش از بازسازی بعدی سیستم توصیه می‌شود.

ذخیره‌سازی Volumeها

یک Named Volume مسیر مشخصی روی هاست ندارد که بتوانید مستقیماً آن را ویرایش کنید؛ بنابراین باید آن را در یک کانتینر موقت mount کرده و از آنجا آرشیو کنید.

docker run --rm \
  -v myapp_uploads:/data:ro \
  -v /srv/backups/myapp:/backup \
  alpine:3 tar czf /backup/uploads.tar.gz -C /data .

این کانتینر کمکی، Volume را به‌صورت فقط‌خواندنی در /data و دایرکتوری پشتیبان شما را در /backup mount می‌کند و سپس آرشیو را در سمت هاست می‌نویسد. دستور --rm بلافاصله پس از خروج tar، کانتینر کمکی را حذف می‌کند. استفاده از :ro اهمیت دارد، زیرا در صورت اشتباه تایپی در دستور tar، منبع اصلی آسیب نمی‌بیند. استفاده از -C /data . باعث می‌شود که بازیابی (restore) در جای درست انجام شود: این سوئیچ تمام مسیرها را نسبت به ریشهٔ Volume ذخیره می‌کند. اگر به‌جای آن tar czf /backup/uploads.tar.gz /data بنویسید، هر مسیر یک data/ در ابتدای خود می‌گیرد و در نتیجه، عملیات بازیابی یک دایرکتوری /data/data درون Volume ایجاد می‌کند که باعث می‌شود برنامه با یک دایرکتوری خالی مواجه شود. مالکیت آرشیو متعلق به root است، زیرا tar درون کانتینر با دسترسی root اجرا شده است. اگر این موضوع برای شما مشکل‌ساز شد، sudo chown "$USER" /srv/backups/myapp/uploads.tar.gz را اجرا کنید و اگر فایل‌های بازیابی‌شده برای برنامه غیرقابل‌خواندن هستند، نحوه تعیین مالکیت فایل توسط PUID و PGID را مطالعه کنید.

این کار را برای هر Named Volume یک‌بار انجام دهید. برای Bind mountها نیازی به کانتینر نیست: tar czf /srv/backups/myapp/config.tar.gz -C /srv/myapp/config . همان کار را مستقیماً روی هاست انجام می‌دهد.

برای هر Volume تصمیم بگیرید که آیا نیاز به متوقف کردن برنامه هست یا خیر. تهیه نسخه پشتیبان tar از Volumeای که برنامه در حال بازنویسی فایل‌های آن است، ممکن است باعث شود فایلی در میانهٔ عملیات نوشتن کپی شود. برای دایرکتوری‌های آپلود که فایل‌ها در آن‌ها یک‌بار نوشته و سپس فقط خوانده می‌شوند، این ریسک ناچیز است. برای سایر موارد، سرویس مربوطه را در طول مدت کپی با docker compose stop app متوقف کنید و سپس docker compose start app را اجرا نمایید. دستور stop کانتینرها و Volumeها را در جای خود باقی می‌گذارد که دقیقاً همان چیزی است که در اینجا نیاز دارید؛ پیش از تایپ هر یک از این دستورات، بهتر است از تفاوت بین down و stop اطمینان حاصل کنید.

هرگز tar گرفتن از Volume دیتابیس را به عنوان پشتیبان دیتابیس در نظر نگیرید. خروجی dump همان پشتیبان اصلی است. آرشیو Volume از یک دیتابیس متوقف‌شده، صرفاً یک روش مفید برای بازسازی سریع است و چیزی فراتر از آن نیست.

ترتیب عملیات

  1. فایل‌های compose و .env را در دایرکتوری پشتیبان کپی کنید.
  2. دیتابیس را در حالی که هنوز در حال اجرا است، Dump کنید.
  3. اگر volumeهای کانتینر برنامه در محل تغییر می‌کنند، آن را متوقف کنید.
  4. از هر volume نام‌گذاری‌شده و هر دایرکتوری bind-mount آرشیو تهیه کنید.
  5. هر سرویسی که متوقف کرده‌اید را دوباره اجرا کنید و سپس با docker compose ps وضعیت را تأیید کنید.
  6. تگ‌ها و digestهای ایمیج‌هایی که stack در حال اجرای آن‌ها است را یادداشت کنید.
  7. کل دایرکتوری پشتیبان را از این سرور به جای دیگری منتقل کنید.

مرحله 7 همان مرحله‌ای است که افراد انجام آن را به بعد موکول می‌کنند.

انتقال نسخه پشتیبان از سرور

داشتن نسخه پشتیبان روی همان دیسکی که داده‌های اصلی قرار دارند، تنها شما را در برابر خطاهای انسانی محافظت می‌کند و در برابر سایر حوادث بی‌اثر است. خرابی یک volume، حذف شدن سرور یا از دست رفتن دسترسی به حساب کاربری، باعث نابودی همزمان هر دو نسخه می‌شود. دایرکتوری مورد نظر را طبق یک زمان‌بندی مشخص و با سیاست نگهداری (retention policy) به فضایی خارج از این VPS منتقل کنید. مقاله پشتیبان‌گیری با restic از یک VPS به تنظیمات مخزن، فلگ‌های مربوط به نگهداری و دستور بررسی سلامت پشتیبان می‌پردازد، بنابراین نیازی به تکرار آن موارد در اینجا نیست.

ابزار restic همچنین می‌تواند dump را مستقیماً از طریق یک pipe بخواند؛ این کار باعث می‌شود دیتابیس به صورت متن ساده (plaintext) هرگز روی دیسک ذخیره نشود:

docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
  | restic backup --stdin --stdin-filename db.dump

از هر ابزاری که استفاده می‌کنید، زمان‌بندی آن را در یک systemd timer یا cron job قرار دهید و تنظیم کنید که در صورت بروز خطا، گزارشی به جایی که آن را می‌بینید ارسال کند. اسکریپتی که خروجی آن به هیچ‌جا ارسال نمی‌شود، اسکریپتی است که ممکن است شش ماه از کار بیفتد و هیچ‌کس متوجه آن نشود.

اثبات عملکرد پشتیبان‌گیری با تمرین بازیابی

پشتیبان‌گیری‌ای که هرگز بازیابی نشده، تنها یک فرضیه است. تمرین زیر، داده‌ها را در یک stack دوم در کنار stack اصلی بازیابی می‌کند تا سرویس‌دهی محیط production مختل نشود و هیچ‌یک از دستورات شما به آن آسیب نرساند.

مکانیزم این کار، نام پروژه است. Docker Compose نام پروژه را از نام دایرکتوری می‌گیرد و آن را روی تمام کانتینرها و volumeهایی که ایجاد می‌کند، درج می‌نماید. با کپی کردن فایل پشتیبان در یک دایرکتوری جدید، stack بازیابی‌شده به‌طور خودکار volumeهای اختصاصی خود را دریافت می‌کند.

sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/myapp-restore
cd /srv/myapp-restore
cp /srv/backups/myapp/compose.yaml /srv/backups/myapp/.env .

فایل compose کپی‌شده را ویرایش کنید تا پورت میزبان (host port) با stack در حال اجرا تداخل نداشته باشد؛ مثلاً از 18080:8080 به‌جای 8080:8080 استفاده کنید یا متغیری که آن را در فایل .env تنظیم می‌کند، تغییر دهید. سپس کانتینرها و volumeهای خالی آن‌ها را بدون اجرای هیچ سرویسی ایجاد کنید:

docker compose create
docker volume ls --filter label=com.docker.compose.project=myapp-restore

دستور دوم باید همان نام‌های volume محیط production را با پیشوند myapp-restore_ فهرست کند. آن‌ها را پر کنید، دیتابیس را به‌تنهایی بالا بیاورید و فایل dump را بارگذاری کنید:

docker run --rm -v myapp-restore_uploads:/data -v /srv/backups/myapp:/backup \
  alpine:3 tar xzf /backup/uploads.tar.gz -C /data
docker compose up -d db
docker compose exec -T db sh -c \
  'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists' \
  < /srv/backups/myapp/db-2026-08-16.dump

سوئیچ --clean --if-exists هر شیء (object) را پیش از ایجاد مجدد حذف می‌کند که باعث می‌شود عملیات بازیابی قابل تکرار باشد. بدون این سوئیچ، اجرای دوم روی دیتابیسی که از قبل شامل آن جداول است، با خطای pg_restore: error: could not execute query: ERROR: relation "users" already exists متوقف می‌شود.

سپس بقیه سرویس‌ها را اجرا کنید و وضعیت آن‌ها را به شکلی که یک کاربر واقعی بررسی می‌کند، چک کنید:

docker compose up -d --wait
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt"'
docker compose logs --tail=50

دستور docker compose up -d --wait تا زمانی که تمام سرویس‌ها در وضعیت running یا healthy قرار نگیرند، منتظر می‌ماند و اگر سرویسی هرگز به این وضعیت نرسد، با کد خروجی غیر صفر خارج می‌شود؛ همین ویژگی باعث می‌شود این مرحله قابل اسکریپت‌نویسی باشد. اگر سرویسی هرگز healthy نشد، docker compose ps وضعیت آن را نشان می‌دهد و بررسی سلامت در Compose توضیح می‌دهد که آن ستون چه چیزی را نمایش می‌دهد. سپس برنامه را روی پورت جایگزین باز کنید و با یک حساب کاربری واقعی وارد شوید. یک رکورد بنویسید و یک فایل که در volume قرار دارد را باز کنید. این دو مورد، اثبات نهایی هستند: dump بازیابی شده، volume بازیابی شده و این دو با هم هماهنگ هستند. تمرینی که فقط نمایش صفحه ورود را اثبات کند، هیچ چیزی را درباره داده‌های شما ثابت نکرده است.

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

docker compose down -v

این تنها جایی است که استفاده از -v به عنوان فلگ، صحیح است. در دایرکتوری production، همین دستور باعث حذف volumeهایی می‌شود که قصد محافظت از آن‌ها را دارید.

نحوه ارتقای یک Compose stack

یادداشت‌های انتشار (release notes) را برای تمام نسخه‌های بین نسخه‌ای که در حال اجرا دارید و نسخه‌ای که قصد ارتقا به آن را دارید مطالعه کنید و به‌دنبال کلمات breaking و migration بگردید. پروژه‌هایی که از پرش بین چندین نسخه اصلی پشتیبانی نمی‌کنند، این موضوع را در آنجا ذکر می‌کنند. همچنین، عملیات migration که با شکست مواجه شود، تنها پس از تغییر بخشی از schema به شما اطلاع می‌دهد.

پیش از ایجاد هرگونه تغییر، وضعیت فعلی خود را ثبت کنید:

docker compose images
docker image inspect --format '{{index .RepoDigests 0}}' postgres:16.4

docker compose images تصویر (image) و تگ (tag) مورد استفاده هر سرویس را در حال حاضر فهرست می‌کند. مقدار digest تنها مقداری است که یک تصویر را به‌طور دقیق مشخص می‌کند، زیرا یک تگ می‌تواند در هر زمانی تغییر کند و به جای دیگری اشاره کند.

از بخش‌های قبلی نسخه پشتیبان تهیه کرده و آن را به خارج از سرور منتقل کنید. این کار را برای نسخه‌های patch نیز انجام دهید. ارتقاهای ارزان‌قیمت همان‌هایی هستند که افراد از آماده‌سازی برای آن‌ها غافل می‌شوند.

سپس نسخه را در فایل compose ثابت (pin) کنید، زیرا latest یک نسخه محسوب نمی‌شود:

services:
  db:
    image: postgres:16.4

با استفاده از image: postgres:latest، دستور docker compose pull هر چیزی که آن تگ امروز به آن اشاره می‌کند را دریافت می‌کند و شما هیچ راهی برای نام‌گذاری آنچه دیروز اجرا می‌کردید نخواهید داشت. یک تگ ثابت، ارتقا را به یک ویرایش تک‌خطی تبدیل می‌کند که می‌توانید در git diff آن را بخوانید و با یک ویرایش دیگر به حالت قبل بازگردانید. تصویر برنامه را نیز به همین ترتیب با استفاده از نسخه دقیق از صفحه انتشار پروژه، ثابت کنید.

تصاویر را دریافت و سرویس‌ها را بازسازی کنید:

docker compose pull
docker compose up -d --wait

دستور docker compose up -d فایل را با کانتینرهای در حال اجرا مقایسه کرده و فقط سرویس‌هایی را که تصویر یا پیکربندی آن‌ها تغییر کرده است، بازسازی می‌کند. این دستور به volumeهای نام‌گذاری‌شده دست نمی‌زند، بنابراین کانتینر جدید روی داده‌های موجود شروع به کار می‌کند. این هدف اصلی این عملیات و در عین حال ریسک آن است، زیرا اولین شروع نسخه جدید معمولاً زمانی است که migration مربوط به schema اجرا می‌شود.

روند کار را زیر نظر بگیرید:

docker compose ps
docker compose logs -f --tail=100 app

کانتینری که با شکست مواجه شده باشد، در ستون STATUS از خروجی docker compose ps وضعیت Exited (1) را نشان می‌دهد و دلیل آن در خطوط آخر لاگ آن قابل مشاهده است. خطاهای migration در آنجا کاملاً واضح هستند و در جای دیگری دیده نمی‌شوند. هنگامی که لاگ‌ها ثابت شدند، وارد برنامه شوید و برای یک دقیقه با آن کار کنید.

اگر docker compose pull با خطای no space left on device متوقف شد، دلیل معمول آن لایه‌های قدیمی تصویر است و پاک‌سازی تصاویر بلااستفاده Docker فضا را آزاد می‌کند. پاک‌سازی را پس از اطمینان از موفقیت‌آمیز بودن ارتقا انجام دهید، نه قبل از آن، زیرا همان لایه‌های قدیمی هستند که یک rollback سریع بر پایه آن‌ها انجام می‌شود.

نحوه بازگشت به نسخه قبل در صورت بروز خطا در ارتقا

دو حالت وجود دارد که هزینه‌های متفاوتی دارند. اگر نسخه جدید تغییری در طرح‌واره (schema) ایجاد نکرده باشد، بازگشت به نسخه قبل تنها یک خط دستور است: تگ قدیمی را در فایل compose قرار دهید و docker compose up -d را اجرا کنید. کانتینر جایگزین می‌شود، volumeها در جای خود باقی می‌مانند و کد قدیمی داده‌هایی را که قبلاً نوشته است، می‌خواند.

اگر نسخه جدید طرح‌واره را تغییر داده باشد، کد قدیمی دیگر قادر به خواندن آن نیست. مهاجرت‌ها (migrations) برای اجرا به سمت جلو نوشته می‌شوند و اکثر پروژه‌ها هیچ اسکریپت بازگشتی (downgrade) ارائه نمی‌دهند؛ بنابراین نسخه قدیمی اجرا شده و در اولین کوئری روی ستونی که تغییر نام یافته یا حذف شده است، با خطاهایی به فرم ERROR: column "avatar_url" does not exist متوقف می‌شود. راه بازگشت، فایلی است که پیش از pull کردن تهیه کرده‌اید (dump): تگ قدیمی را بازگردانید، volume دیتابیس را حذف کنید، آن را به‌صورت خالی دوباره بسازید، dump را در آن بازیابی کنید و سرویس را استارت بزنید. بدون آن dump، هیچ راه بازگشتی وجود ندارد و به همین دلیل است که تهیه نسخه پشتیبان پیش از pull کردن الزامی است.

نسخه‌های اصلی (major versions) در Postgres حساس‌ترین حالت هستند و معمولاً کاربران را غافلگیر می‌کنند، زیرا خطا نه در زمان بازگشت، بلکه در زمان ارتقا رخ می‌دهد. فرمت ذخیره‌سازی روی دیسک با هر نسخه اصلی تغییر می‌کند. اگر postgres:16.4 را به postgres:17.2 تغییر دهید و docker compose up -d را اجرا کنید، سرور جدید از شروع به کار امتناع می‌کند:

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.2.

ایمج دیتابیس به صورت خودکار pg_upgrade را برای شما انجام نمی‌دهد. مسیر پشتیبانی‌شده در یک stack از نوع Compose عبارت است از: dump گرفتن، جایگزینی و بازیابی. در حالی که نسخه قدیمی در حال اجراست dump بگیرید، docker compose down را اجرا کنید، volume دیتابیس را حذف کنید، تگ جدید را تنظیم کنید، docker compose create را برای ایجاد یک دایرکتوری داده خالی اجرا کنید، دیتابیس را استارت بزنید، dump را بازیابی کنید و سپس بقیه سرویس‌ها را بالا بیاورید. dump قدیمی را تا زمانی که نسخه اصلی جدید حداقل یک روز ترافیک واقعی را مدیریت نکرده است، نگه دارید. ارتقاهای جزئی (minor) در یک نسخه اصلی، مثلاً از 16.4 به 16.9، به هیچ‌کدام از این مراحل نیاز ندارند، زیرا فرمت داده در آن‌ها پایدار است و کانتینر به‌سادگی اجرا می‌شود.

آیا اسنپ‌شات‌های VPS همان نسخه پشتیبان (Backup) هستند؟

آن‌ها مکمل یکدیگرند و هر کدام در شرایط متفاوتی شکست می‌خورند. اسنپ‌شات، کل دیسک را در سطح هایپروایزر کپی می‌کند، بنابراین کل ماشین را در عرض چند دقیقه به حالت قبل بازمی‌گرداند، از جمله بخش‌هایی که فراموش کرده‌اید از آن‌ها نسخه پشتیبان تهیه کنید. این ویژگی، آن را به ابزاری مناسب برای یک کار خاص تبدیل می‌کند: ارتقای سیستم باعث خرابی سرور شده و شما می‌خواهید آن را به وضعیت 20 دقیقه پیش بازگردانید.

برای سایر موارد، این ابزار مناسبی نیست. سطح دسترسی آن کل ماشین است، بنابراین بازیابی یک جدول حذف‌شده به معنای بازگردانی کل سرور در جایی دیگر و استخراج دستی آن جدول از درون آن است. دوره نگهداری اسنپ‌شات‌ها معمولاً کوتاه است. این نسخه‌ها معمولاً در همان حساب کاربری ارائه‌دهنده سرور ذخیره می‌شوند، بنابراین از دست رفتن حساب کاربری به معنای از دست رفتن همزمان سرور و اسنپ‌شات‌های آن است. همچنین، اسنپ‌شاتِ یک ماشین در حال اجرا، دیتابیس را در حین نوشتن ثبت می‌کند؛ در نتیجه دیتابیس هنگام اولین اجرا عملیات crash recovery را انجام می‌دهد و هر تراکنشی که در حال پردازش بوده، از بین می‌رود.

از هر دو استفاده کنید. اسنپ‌شات دکمه «بازگشت» (Undo) برای بازه زمانی ارتقای سیستم است. فایل dump همان نسخه‌ای است که در صورت حذف حساب کاربری باقی می‌ماند. تفاوت اسنپ‌شات‌ها و نسخه‌های پشتیبان بررسی می‌کند که هر کدام واقعاً چه نوع خرابی‌هایی را پوشش می‌دهند. همان دایرکتوری پشتیبان، چیزی است که انتقال یک استک به یک VPS جدید را به یک کار روتین تبدیل می‌کند، نه بازسازی سیستم از روی حافظه.

چه چیزی ممکن است اشتباه پیش برود و چه چیزی مشاهده خواهید کرد

استفاده از فلگ volumes در دستور down، یعنی docker compose down -v، حجم‌های (volumes) نام‌گذاری‌شده‌ای که در فایل تعریف شده‌اند را حذف می‌کند و Compose این موضوع را با خطی شامل Volume myapp_db_data Removed تأیید می‌کند. این عملیات قابل بازگشت نیست. دستور سادهٔ docker compose down آن‌ها را دست‌نخورده باقی می‌گذارد. از فرم طولانی یعنی docker compose down --volumes استفاده کنید تا فلگ مخرب، کلمه‌ای باشد که مجبور به تایپ کامل آن شوید.

دامپی بدون رشتهٔ جادویی. خطای pg_restore: error: did not find magic string in file header به این معنی است که فایل یک آرشیو نیست. دلیل معمول آن، نبود -T در دستور docker compose exec است؛ زیرا وقتی TTY متصل باشد، جریان داده در مسیر رسیدن به shell شما ترجمه می‌شود و دامپ باینری آسیب می‌بیند. دامپ را دوباره با -T بگیرید و سپس پنج بایت اول را با head -c 5 بررسی کنید.

گذرواژه‌ای که تغییر نمی‌کند. خطای FATAL: password authentication failed for user "appuser" پس از بازیابی (restore) به این معنی است که .env و دایرکتوری داده از زمان‌های متفاوتی هستند. image فقط زمانی گذرواژه را تنظیم می‌کند که یک دایرکتوری دادهٔ خالی ایجاد کند، بنابراین ویرایش .env در مراحل بعدی، هیچ تغییری در داخل پایگاه داده ایجاد نمی‌کند. .env منطبق را بازیابی کنید یا گذرواژه را از داخل پایگاه داده با ALTER USER تغییر دهید.

یک حجم دوم و خالی. داکر در صورت نیاز یک حجم ایجاد می‌کند، بنابراین docker run -v myapp_upload:/data در حالی که s وجود ندارد، داده‌ها را در یک حجم جدید و خالی می‌نویسد و گزارش موفقیت می‌دهد. دستور docker volume ls سپس هر دو نام را نشان می‌دهد که یکی از آن‌ها خالی است. نام حجم‌ها را از docker volume ls کپی کنید و از حفظ تایپ نکنید.

بازیابی روی محیط عملیاتی (production). اجرای دستورات بازیابی در /srv/myapp به‌جای /srv/myapp-restore، داده‌های زنده را با نسخهٔ پشتیبان بازنویسی می‌کند و دستورات در هر دو مکان یکسان به نظر می‌رسند. قبل از هر دستور بازیابی، pwd را بررسی کنید و محیط تمرین را در دایرکتوری جداگانه‌ای نگه دارید.

FAQ

آیا docker compose down داده‌های من را حذف می‌کند؟

خیر. docker compose down فقط کانتینرها و شبکه پیش‌فرض را حذف می‌کند و به Volumeهای نام‌گذاری‌شده و Bind mountها دست نمی‌زند. docker compose down -v آن Volumeهای نام‌گذاری‌شده‌ای را که در فایل خود تعریف کرده‌اید حذف می‌کند و این عملیات غیرقابل بازگشت است. Bind mountها دایرکتوری‌های میزبان هستند، بنابراین Compose هرگز آن‌ها را حذف نمی‌کند. اگر می‌خواهید سرویس‌ها را برای پشتیبان‌گیری متوقف کنید و همه چیز دست‌نخورده باقی بماند، از docker compose stop استفاده کنید.

آیا می‌توانم به‌جای اجرای pg_dump، دایرکتوری داده‌های Postgres را کپی کنم؟

فقط زمانی که کانتینر متوقف شده باشد. در حالی که سرور در حال اجراست، فایل‌های آن تغییر می‌کنند و کپی حاصل ممکن است شامل ترکیبی از وضعیت‌های مختلف باشد که قابل بازیابی نیست. کپی در سطح فایل همچنین به یک نسخه اصلی (major version) خاص از Postgres وابسته است، بنابراین در نسخه دیگری اجرا نخواهد شد. کانتینر را متوقف کنید، از Volume آرشیو بگیرید، دوباره آن را اجرا کنید و نتیجه را به‌عنوان یک مسیر بازسازی سریع در نظر بگیرید، نه تنها نسخه پشتیبان خود. فایل dump نسخه قابل‌حمل است و همان فایلی است که باید از آن بازیابی انجام دهید.

چگونه Postgres را به یک نسخه اصلی جدید در Compose ارتقا دهم؟

تغییر تگ کافی نیست. سرور جدید از اجرای روی دایرکتوری داده‌های قدیمی خودداری می‌کند و خطای The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.2 را در لاگ ثبت می‌کند. در حالی که نسخه قدیمی در حال اجراست، pg_dump را اجرا کنید، سپس docker compose down را انجام دهید، Volume دیتابیس را حذف کنید، تگ جدید را تنظیم کنید، docker compose create را برای ایجاد یک Volume خالی جدید اجرا کنید، دیتابیس را استارت بزنید و dump را در آن بازیابی کنید. dump قدیمی را تا زمانی که نسخه جدید ترافیک واقعی را مدیریت نکرده است، نگه دارید.

پشتیبان‌گیری باید هر چند وقت یک‌بار انجام شود و چه مدت باید آن‌ها را نگه دارم؟

بازه زمانی را بر اساس میزان کاری که حاضرید دوباره انجام دهید، تعیین کنید. پشتیبان‌گیری شبانه برای یک استک شخصی یا تیم کوچک مناسب است، به‌علاوه یک پشتیبان دستی اضافه بلافاصله قبل از هر ارتقا. برای نگهداری، تاریخچه‌ای کافی نگه دارید تا آسیب‌هایی که بلافاصله متوجه آن‌ها نشده‌اید را پوشش دهید، زیرا اگر روز جمعه متوجه خرابی یک جدول شوید، نسخه پشتیبان پنج‌شنبه شب کمکی نخواهد کرد. restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune یک سیاست شروع معقول است. فارغ از زمان‌بندی، هر فصل یک‌بار از روی پشتیبان بازیابی انجام دهید. تا زمانی که این کار را انجام نداده‌اید، شما پشتیبان ندارید، فقط تعدادی فایل دارید.

آیا برای تهیه پشتیبان باید کل استک را متوقف کنم؟

معمولاً خیر. dump دیتابیس در حالی که سرور در حال اجراست، منسجم باقی می‌ماند، بنابراین دیتابیس نیازی به downtime ندارد. Volumeها موضوع اصلی هستند. اگر برنامه فقط فایل اضافه می‌کند، مانند دایرکتوری آپلودها، آرشیو کردن در حالت زنده (live) به اندازه کافی ایمن است. اگر برنامه فایل‌ها را در جای خود بازنویسی می‌کند، آن سرویس خاص را به مدت زمان کپی با docker compose stop app متوقف کنید و پس از اتمام کار دوباره آن را استارت بزنید. متوقف کردن برنامه در حالی که دیتابیس همچنان در حال اجراست، معمولاً کوتاه‌ترین بازه زمانی ایمنی است که می‌توانید ایجاد کنید.