عندما يحين وقت الانتقال إلى سيرفر Server جديد، لأن السيرفر الحالي لم يعد يكفي أو لأنك تريد تغيير المزود أو تريد نظام تشغيل أحدث، فسوف تجد أن المشكلة ليست في السيرفر الجديد نفسه، وإنما في ما تراكم على القديم: Containers تعمل منذ سنوات، وقواعد بيانات Databases وملفات تجمعت داخل ال Volumes، وإعدادات صغيرة لا تتذكرها إلا عندما يتوقف شيء، وبالتالي فإعادة تثبيت كل تطبيق من الصفر قد تأخذ منك أياماً، وفي الغالب سوف تنسى فيها شيئاً لن تكتشفه إلا بعد أن يشتكي المستخدمون.
ولذلك سوف نشرح في هذا الدليل الترحيل Migration بطريقتين، الأولى تنقل بيانات Docker كاملة دفعة واحدة، أي المجلد /var/lib/docker ومعه كل مجلد على السيرفر تستخدمه ال Containers عبر Bind Mount، فتعمل التطبيقات على السيرفر الجديد كما كانت تماماً، والثانية تنقل كل مشروع Docker Compose وحده مع بياناته، وهي أنظف وأكثر مرونة، وفي الطريقتين يبقى السيرفر القديم سليماً حتى تتأكد أن كل شيء يعمل.
وسوف نناقش في هذا المقال ما يلي:
- كيف تختار بين الطريقتين، وما الذي يجب أن تفحصه على السيرفرين قبل أن تنقل أي شيء، خصوصاً طريقة التخزين التي تغيرت في Docker 29.
- لماذا يفشل النسخ البسيط للمجلد
/var/lib/docker، وما هي الخيارات التي تحتاجها معtarوrsyncحتى تصل البيانات كما هي. - نقل كل تطبيق مع Compose، وتصدير قاعدة البيانات Database Dump بدل نسخ ملفاتها، ثم استعادتها على السيرفر الجديد.
- تحويل DNS، والتحقق من النجاح، وخطة التراجع، وأشهر المشكلات وحلولها.
أي الطريقتين تناسبك؟
الطريقة الأولى تناسبك إذا كان على السيرفر عدد كبير من ال Containers، وبعضها لم ينشأ بملف Compose أصلاً، ولا تريد أن تراجع كل تطبيق وحده، ولكنها تنسخ حالة Docker الداخلية كما هي، لذلك يجب أن تتطابق البيئة بين السيرفرين. أما الطريقة الثانية فتناسب السيرفر المنظم في مشاريع Compose، وتعمل حتى لو اختلفت معمارية المعالج CPU Architecture أو طريقة التخزين بين السيرفرين، والجدول التالي يلخص الفرق بينهما:
| النسخ الكامل لبيانات Docker | النقل لكل تطبيق مع Compose | |
|---|---|---|
| ما ينقل | كل ال Containers وال Images وال Volumes والشبكات دفعة واحدة | مجلد المشروع و.env وبيانات ال Volumes أو تفريغ قاعدة البيانات |
| إصدار Docker Engine | الإصدار نفسه أو أحدث منه | أي إصدار حديث |
| إعداد التخزين والمعمارية | يجب أن يتطابقا | لا يشترط التطابق |
| مدة التوقف (Downtime) | كل التطبيقات معاً، من إيقاف Docker حتى التشغيل على السيرفر الجديد | كل تطبيق على حدة، ويمكن توزيع النقل على أكثر من نافذة صيانة |
| يناسب | سيرفراً قديماً مزدحماً تريد نقله كما هو | سيرفراً منظماً، أو فرصة لترتيب السيرفر من جديد |
ما تحتاجه قبل أن تبدأ
- سيرفر جديد يعمل بنظام Linux، ومثبت عليه Docker Engine مع Compose، ودليل تثبيت Docker على Ubuntu يشرح التثبيت من المستودع الرسمي، أما دليل تأمين خادم VPS من أول دخول فيشرح ما يجب أن تفعله قبل أن تنقل إليه أي بيانات.
- مستخدم يملك صلاحية
sudoعلى السيرفرين، ودخول عبر SSH من السيرفر القديم إلى الجديد، وسوف نستخدم في هذا الدليل المستخدمdeployوالعنوان التوضيحيnew-server.example.com(أو203.0.113.20). - مساحة كافية على القرص Disk، أي مكان للأرشيف Archive على السيرفر القديم، ومكان للأرشيف وللبيانات بعد فكه على الجديد.
- نافذة صيانة Maintenance Window يقبل فيها المستخدمون توقف التطبيقات، وصلاحية تعديل سجلات DNS للنطاقات Domains التي تشير إلى السيرفر.
افحص السيرفرين قبل أن تبدأ
أغلب حالات الفشل في النسخ الكامل لا تظهر إلا بعد فك الأرشيف، فإما أن تختفي ال Images ظاهرياً أو يرفض Docker أن يبدأ، لذلك قارن السيرفرين أولاً بتنفيذ هذه الأوامر على كل منهما:
docker version -f '{{ .Server.Version }}'
docker info -f '{{ .Architecture }}'
docker info -f '{{ .DockerRootDir }}'
docker info -f '{{ .Driver }} {{ .DriverStatus }}'في الأوامر أعلاه لاحظ التالي:
- الإصدار Version: يجب أن يكون Docker Engine على السيرفر الجديد بنفس الإصدار أو أحدث منه، ولا تنقل البيانات أبداً إلى إصدار أقدم، والسبب أن الإصدار القديم لا يضمن أن يفهم ملفات كتبها إصدار أحدث، وإذا أردت نفس الإصدار تماماً فصفحة التثبيت على Ubuntu تشرح تثبيت إصدار محدد بالأمر
apt list --all-versions docker-ce. - المعمارية Architecture: يجب أن تتطابق، أي
x86_64معx86_64أوaarch64معaarch64، والسبب أن ال Image المحفوظة على القرص مبنية لمعمارية واحدة، فلن تعمل على معالج من نوع آخر. - مجلد البيانات Data Root: القيمة المعتادة هي
/var/lib/docker، فإذا ظهر مسار آخر فهذا يعني أن أحدهم غيره بالخيارdata-root، وعليك أن تضعه مكان/var/lib/dockerفي كل أوامر هذا الدليل. - طريقة التخزين Storage Backend: وهذه النقطة يغفل عنها كثيرون، لذلك نشرحها في الفقرة التالية.
لماذا يجب أن تتطابق طريقة التخزين؟
منذ الإصدار 29.0 يستخدم Docker Engine في التثبيت الجديد مخزن ال Images الخاص بـ containerd وليس مشغل التخزين التقليدي Storage Driver overlay2، ولكن السيرفر الذي قمت بتحديثه Upgrade من إصدار أقدم يبقى على overlay2، والمشكلة أن مكان البيانات يختلف بين الحالتين كما تشرح وثائق Docker، فمع overlay2 تكون كل البيانات في /var/lib/docker، أما مع containerd فتكون ال Images وطبقات Layers ال Containers في /var/lib/containerd، وتبقى ال Volumes والإعدادات في /var/lib/docker.
لذلك انظر في مخرج الأمر الأخير، فإذا ظهر فيه io.containerd.snapshotter.v1 فالسيرفر يستخدم containerd، وإلا فهو على overlay2 ويظهر اسمه في بداية السطر. ولاحظ أن الوثائق تذكر أن التبديل بين الطريقتين يخفي ال Images وال Containers التي أنشئت بالطريقة الأخرى، فتبدو وكأنها حذفت، وهي في الحقيقة ما زالت على القرص.
وبالتالي إذا كان السيرفر القديم على overlay2 والجديد تثبيتاً حديثاً بالإصدار 29، فعليك أن توقف خيار containerd على السيرفر الجديد في ملف daemon.json قبل النقل، وإذا كان الملف موجوداً فأضف إليه المفتاح features ولا تستبدله:
sudo tee /etc/docker/daemon.json <<'EOF'
{
"features": {
"containerd-snapshotter": false
}
}
EOF
sudo systemctl restart docker
docker info -f '{{ .Driver }} {{ .DriverStatus }}'بعد ذلك انقل يدوياً ما تحتاجه من إعدادات /etc/docker/daemon.json الموجود على السيرفر القديم، مثل إعدادات السجلات Logs وlive-restore التي شرحناها في دليل التثبيت، ولا تنسخ الملف القديم فوق هذا الإعداد دون مراجعة، والسبب أنك سوف تمسح المفتاح features الذي أضفته للتو.
كم مساحة تحتاج، وكم سيتوقف التطبيق؟
احسب حجم البيانات على السيرفر القديم، والمساحة المتاحة على السيرفرين، كما يلي:
docker system df
sudo du -sh /var/lib/docker /var/lib/containerd
df -h /وهذا الحجم هو الذي يحدد مدة التوقف تقريباً، لأن وقت إنشاء الأرشيف ونقله وفكه يزيد معه، وإذا كانت التطبيقات مهمة فقم بتخفيض مدة صلاحية سجلات DNS واختصاراً TTL إلى 300 ثانية قبل النقل بيوم، وبهذا الشكل يصل التحويل إلى السيرفر الجديد خلال دقائق وليس خلال ساعات، والسبب شرحناه في شرح DNS وسجلاته للمبتدئين.
الطريقة الأولى: نسخ بيانات Docker كاملة
قد تبدو العملية من الوهلة الأولى بسيطة، فالحل الذي يخطر على البال أولاً هو أن تنسخ المجلد /var/lib/docker إلى السيرفر الجديد وانتهى الأمر، ولكن في الواقع فهذا الحل يفشل بأكثر من طريقة: فهو لا ينقل المجلدات التي تربطها ال Containers من خارج هذا المجلد، وأدوات النسخ المعتادة تضيع أرقام المالكين والسمات الموسعة Extended Attributes والروابط الصلبة Hard Links التي يعتمد عليها Docker، وإذا كان live-restore مفعلاً فسوف تبقى ال Containers تكتب في البيانات أثناء النسخ حتى بعد أن توقف خدمة Docker. لذلك فالطريقة الصحيحة هي أن توقف ال Containers ثم Docker على السيرفر القديم حتى لا تتغير البيانات أثناء النسخ، ثم تضع في أرشيف واحد المجلد /var/lib/docker وكل مجلد على السيرفر تستخدمه ال Containers مع الخيارات التي تحفظ كل ما سبق، وبعد ذلك تنقل الأرشيف وتفكه على السيرفر الجديد في نفس المواضع.
اعرف المجلدات التي تستخدمها ال Containers من خارج Docker
لا تكتف بنسخ /var/lib/docker وحده، والسبب أن ال Volumes المسماة Named Volumes موجودة داخله فعلاً، ولكن ال Bind Mount يربط مجلداً من أي مكان على السيرفر، مثل /opt/myapp/data، وهذا المجلد خارج /var/lib/docker. والأمر docker inspect يعرض مصدر كل ربط، لذلك اجمع هذه المصادر قبل إيقاف Docker، لأن الأمر يحتاج إلى الخدمة وهي تعمل:
docker ps -aq | xargs -r docker inspect --format '{{ range .Mounts }}{{ if eq .Type "bind" }}{{ .Source }}{{ "\n" }}{{ end }}{{ end }}' | sort -u | grep -v -E '^$|^(/etc/localtime|/etc/timezone|/var/run/docker.sock|/run/docker.sock)$' > host_paths.txt
cat host_paths.txtفي الأمر أعلاه لاحظ التالي:
- الخيار
-aيشمل ال Containers المتوقفة أيضاً، لأن بعضها قد لا يعمل إلا عند الحاجة، مثل Container للنسخ الاحتياطي يعمل مرة في الليلة. - الأمر يستبعد ملفات تخص النظام نفسه، فالملفان
/etc/localtimeو/etc/timezoneموجودان أصلاً على السيرفر الجديد، أماdocker.sockفهو مقبس Socket ينشئه Docker عند تشغيله، فلا معنى لنسخه. - راجع القائمة بعينك قبل أن تتابع، وإذا ظهر فيها مسار واسع مثل
/homeأو/فقرر بنفسك هل تريد نسخه كاملاً.
بعد ذلك أنشئ قائمة النسخ الكاملة، وأضف إليها /var/lib/containerd إذا كان السيرفر القديم يستخدم containerd:
printf '%s\n' /var/lib/docker > backup_paths.txt
cat host_paths.txt >> backup_paths.txt
echo /var/lib/containerd >> backup_paths.txt
cat backup_paths.txtإيقاف Docker على السيرفر القديم
من هذه اللحظة يبدأ التوقف. وقبل أن توقف الخدمة، عليك أن توقف ال Containers نفسها، والسبب أن دليل التثبيت يفعل الخيار live-restore، ومع هذا الخيار تبقى ال Containers عاملة وتكتب في ال Volumes حتى بعد أن تتوقف خدمة Docker، وبالتالي سوف تنسخ قاعدة بيانات تتغير أثناء النسخ. لذلك احفظ أسماء ال Containers التي تعمل الآن في ملف، ثم أوقفها، ثم انقل الملف إلى السيرفر الجديد لأنك سوف تحتاجه هناك:
docker info -f '{{ .LiveRestoreEnabled }}'
docker ps --format '{{ .Names }}' > running_containers.txt
xargs -r docker stop < running_containers.txt
rsync -a running_containers.txt [email protected]:/home/deploy/الآن أوقف المقبس docker.socket مع الخدمة، وإلا فسوف يعيد systemd تشغيل Docker عند أول أمر يصله، ثم أوقف containerd أيضاً حتى لا تتغير ملفاته أثناء النسخ:
sudo systemctl stop docker.socket docker
sudo systemctl stop containerd
systemctl is-active docker containerdويجب أن تظهر الكلمة inactive مرتين.
إنشاء الأرشيف
sudo tar --create --gzip --file ~/full_backup.tar.gz --numeric-owner --xattrs --xattrs-include='*' --acls --files-from backup_paths.txt
sudo chown "$USER": ~/full_backup.tar.gz
chmod 600 ~/full_backup.tar.gzفي الإعداد أعلاه لاحظ التالي، فلكل خيار سببه:
--files-fromيقرأ المسارات من الملف سطراً سطراً، وبالتالي لا تنكسر المسارات التي فيها مسافات، كما كان سوف يحدث لو كتبت$(cat host_paths.txt).--numeric-ownerيحفظ أرقام المالكين وليس أسماءهم، والسبب أن المستخدمpostgresمثلاً قد يحمل رقماً آخر على السيرفر الجديد أو لا يكون موجوداً عليه أصلاً، أما التطبيقات داخل ال Containers فتعتمد على الرقم وحده.--xattrs --xattrs-include='*'يحفظ السمات الموسعة، حيث يعتمدoverlay2على السمةtrusted.overlay.opaqueفي نواة Linux Kernel ليعرف المجلدات التي حذفت داخل ال Container، ولكن tar عند فك الأرشيف لا يستعيد افتراضياً إلا سمات النوع user، لذلك نطلب كل الأنواع صراحة، وإذا نسيت هذا الخيار فسوف تفقد هذه السمة بصمت دون أي رسالة خطأ.--aclsيحفظ قوائم التحكم بالوصول Access Control Lists واختصاراً ACLs إن وجدت.
وسوف تظهر لك الرسالة Removing leading `/' from member names، وهذا طبيعي لأن tar يحفظ المسارات نسبية، ونحن سوف نفكها لاحقاً في /. بعد ذلك سجل بصمة الأرشيف Checksum حتى تقارنها بعد النقل:
sha256sum ~/full_backup.tar.gz.env وكلمات مرور قواعد البيانات والمفاتيح الخاصة Private Keys لشهادات TLS، ولذلك قصرنا صلاحية قراءته على مالكه بالأمر chmod 600، فلا تضعه في مجلد مشترك، واحذفه من السيرفرين بعد انتهاء النقل والتحقق.نقل الأرشيف إلى السيرفر الجديد
rsync -a --partial --progress ~/full_backup.tar.gz [email protected]:/home/deploy/الخيار --partial يحتفظ بالجزء الذي وصل إذا انقطع الاتصال، فتستأنف النقل بدل أن تبدأ من الصفر، وهذا مهم مع أرشيف بعشرات الجيجابايت. أما الخيار -z فلا فائدة منه هنا، لأن الأرشيف مضغوط أصلاً. وعلى السيرفر الجديد تأكد أن البصمة تطابق ما سجلته:
sha256sum /home/deploy/full_backup.tar.gzبديل: النسخ المباشر بـ rsync دون أرشيف
إذا لم يكن على السيرفر القديم مكان لأرشيف بحجم البيانات، فانسخ المجلدات مباشرة إلى السيرفر الجديد. وقد يتساءل البعض: لماذا لا نكتفي بالأمر المعروف rsync -a؟ والإجابة أن الخيار -a وحده لا يشمل الروابط الصلبة ولا قوائم ACL ولا السمات الموسعة، وبالتالي سوف تصل الملفات ولكن بدون ما كان يحفظه tar في الطريقة السابقة، لذلك نضيف الخيارات التالية:
-Hيحفظ الروابط الصلبة، و-Aيحفظ قوائم ACL، و-Xيحفظ السمات الموسعة.--numeric-idsينقل أرقام المالكين كما هي ولا يطابقها بالأسماء على الطرفين، لنفس السبب الذي شرحناه مع--numeric-owner.-rيجب أن تذكره صراحة، والسبب أن--files-fromيلغي التكرار Recursion الذي يتضمنه-aعادة.
ولأن rsync يحتاج إلى صلاحيات root على الطرفين حتى يحفظ الملكية Ownership، فهذا البديل يتطلب أمرين مؤقتين: الأول أن يسمح sudo على السيرفر الجديد للمستخدم deploy بتشغيل rsync دون كلمة مرور، حيث تنشئ لذلك ملفاً بالأمر sudo visudo -f /etc/sudoers.d/rsync-migration وتضع فيه السطر deploy ALL=(root) NOPASSWD: /usr/bin/rsync، والثاني مفتاح SSH على السيرفر القديم يسمح بالدخول إلى السيرفر الجديد. وبعد إيقاف Docker نفذ الأمر التالي:
sudo rsync -aHAX -r --numeric-ids --info=progress2 -e "ssh -i /home/deploy/.ssh/id_ed25519" --rsync-path="sudo rsync" --files-from=backup_paths.txt / [email protected]:/ولاحظ أن الأمر يعمل بصلاحيات sudo، لذلك سوف يسألك ssh عند أول اتصال عن بصمة السيرفر الجديد حتى لو كنت قد قبلتها من قبل بمستخدمك العادي. وقبل تنفيذه أوقف Docker على السيرفر الجديد وانقل مجلداته الحديثة جانباً كما في الخطوة التالية، وعندما ينتهي النقل احذف ملف sudoers والمفتاح المؤقت فوراً.
الاستعادة على السيرفر الجديد
أوقف Docker على السيرفر الجديد، وانقل جانباً المجلدات التي أنشأها التثبيت الحديث بدل أن تفك الأرشيف فوقها، والسبب أنك بهذا الشكل لا تخلط بين حالتين مختلفتين، وتبقى عندك نسخة نظيفة تعود إليها إذا احتجت:
sudo systemctl stop docker.socket docker
sudo systemctl stop containerd
sudo mv /var/lib/docker /var/lib/docker.fresh
sudo mv /var/lib/containerd /var/lib/containerd.freshثم فك الأرشيف في / بنفس الخيارات، فيرجع كل مجلد إلى مساره الأصلي:
sudo tar --extract --gzip --file /home/deploy/full_backup.tar.gz --numeric-owner --xattrs --xattrs-include='*' --acls --directory /وإذا لم يكن السيرفر القديم يستخدم containerd، فلن تجد /var/lib/containerd في الأرشيف، لذلك أعد المجلد الحديث إلى مكانه بالأمر sudo mv /var/lib/containerd.fresh /var/lib/containerd، ثم شغل الخدمتين:
sudo systemctl start containerd
sudo systemctl start docker
docker ps -aال Containers التي لها سياسة إعادة التشغيل Restart Policy بالقيمة always تبدأ تلقائياً مع Docker، أما التي لها القيمة unless-stopped فلن تبدأ، والسبب أننا أوقفناها يدوياً قبل النسخ، والوثائق تذكر أن هذه السياسة لا تعيد تشغيل Container أوقفته بنفسك حتى بعد إعادة تشغيل Docker. لذلك شغل كل ما كان يعمل على السيرفر القديم من الملف الذي نقلته:
xargs -r docker start < /home/deploy/running_containers.txt
docker psوأي Container آخر تحتاجه شغله بالأمر docker start، أو بالأمر docker compose up -d داخل مجلد كل مشروع.
الطريقة الثانية: نقل كل تطبيق على حدة مع Compose
إذا كانت تطبيقاتك منظمة في مشاريع Compose، فكل ما تحتاجه لإعادة بناء التطبيق موجود في مكانين: الأول مجلد المشروع، وفيه docker-compose.yml و.env وأي Bind Mount نسبي مثل ./data، والثاني ال Volumes المسماة. أما ال Images فسوف تسحبها من جديد على السيرفر الجديد، ولأنك ثبت أرقام الإصدارات في الملف فسوف تحصل على نفس الإصدار بالضبط.
لنفرض أن لديك مشروعاً في /opt/myapp باسم myapp، فيه خدمة app وقاعدة بيانات باسم الخدمة db، فنفذ الخطوات التالية على السيرفر القديم داخل مجلد المشروع:
cd /opt/myapp
mkdir -p migration
docker volume ls --filter label=com.docker.compose.project=myappقاعدة البيانات: صدرها إلى ملف أولاً
أمامك خياران مع قاعدة البيانات، إما أن تنسخ ملفاتها من ال Volume، أو أن تفرغها إلى ملف Database Dump ثم تستعيدها، والتفريغ هو الأسلم عند النقل، والسبب أنه لا يعتمد على تطابق ملفات قاعدة البيانات بين السيرفرين، ويكشف لك أي تلف قبل أن تحذف شيئاً. لذلك أوقف التطبيق أولاً حتى لا يكتب بيانات جديدة أثناء التفريغ، واترك قاعدة البيانات تعمل.
مع PostgreSQL، الخيار -Fc يحفظ التفريغ بصيغة مخصصة Custom Format تقرؤها الأداة pg_restore:
docker compose stop app
docker compose exec -T db pg_dump -U app -d app -Fc > migration/app.dumpومع MySQL، الخيار --single-transaction يأخذ نسخة متسقة Consistent لجداول InnoDB دون أن يقفلها Lock، ونقرأ كلمة المرور من متغيرات البيئة Environment Variables داخل ال Container حتى لا تظهر في سجل الأوامر Shell History:
docker compose stop app
docker compose exec -T db sh -c 'exec mysqldump --single-transaction --routines --triggers -uroot -p"$MYSQL_ROOT_PASSWORD" app' > migration/app.sqlوإذا كانت قاعدة البيانات MariaDB بالإصدار 11 أو أحدث، فسوف تجد أن ال Image الرسمية لا تحتوي على الأمرين mysqldump وmysql، لذلك استخدم مكانهما mariadb-dump وmariadb بنفس الخيارات.
تأكد أن الملف ليس فارغاً بالأمر ls -lh migration/، ثم أوقف المشروع كاملاً، واستخدم stop وليس down -v، والسبب أن الخيار -v يحذف ال Volumes، وأنت تحتاجها إذا قررت التراجع:
docker compose stopنسخ ال Volumes الأخرى
ال Volumes التي ليس فيها قاعدة بيانات، مثل الملفات المرفوعة، لها طريقة بسيطة في وثائق Docker، حيث تشغل Container مؤقتاً يربط ال Volume للقراءة فقط ويربط مجلداً على السيرفر، ثم يضع محتوى ال Volume في أرشيف داخل ذلك المجلد:
docker run --rm -v myapp_app-data:/data:ro -v "$PWD/migration":/backup ubuntu:24.04 tar --create --gzip --numeric-owner --file /backup/app-data.tar.gz -C /data .كرر الأمر لكل Volume ظهر في القائمة، وغير اسم ال Volume واسم الملف في كل مرة، ولا تنسخ Volume قاعدة البيانات إذا كنت قد فرغتها، فطريقة واحدة تكفي.
نقل مجلد المشروع
أصبح مجلد المشروع الآن يحتوي على كل شيء، أي ملف Compose و.env وال Bind Mounts النسبية، ومجلد migration بما فيه من تفريغ وأرشيفات، لذلك ضعه في أرشيف واحد مع حفظ الملكية ثم انقله:
sudo tar --create --gzip --numeric-owner --xattrs --acls --file ~/myapp.tar.gz -C /opt myapp
sudo chown "$USER": ~/myapp.tar.gz
chmod 600 ~/myapp.tar.gz
rsync -a --partial --progress ~/myapp.tar.gz [email protected]:/home/deploy/إعادة بناء المشروع على السيرفر الجديد
فك الأرشيف، ثم اطلب من Compose أن ينشئ ال Containers وال Volumes دون أن يشغلها، وبهذا الشكل تنشأ ال Volumes بالأسماء والوسوم Labels التي يتوقعها Compose، ثم تملؤها أنت بالبيانات:
sudo tar --extract --gzip --numeric-owner --xattrs --acls --file /home/deploy/myapp.tar.gz -C /opt
cd /opt/myapp
docker compose pull
docker compose createبعد ذلك استعد كل Volume بنفس الطريقة ولكن في الاتجاه المعاكس:
docker run --rm -v myapp_app-data:/data -v "$PWD/migration":/backup:ro ubuntu:24.04 tar --extract --gzip --numeric-owner --file /backup/app-data.tar.gz -C /dataالآن شغل قاعدة البيانات وحدها، وانتظر حتى تجهز بالخيار --wait، ولكن لا تعتمد على هذا الخيار وحده هنا، والسبب أن ال Volume جديد وفارغ، فتبدأ ال Image الرسمية لـ PostgreSQL وMySQL بتهيئة قاعدة البيانات عبر سيرفر مؤقت لا يقبل إلا الاتصال المحلي عبر ال Socket، ثم توقفه وتشغل السيرفر الحقيقي، وقد ينجح فحص الصحة Health Check أثناء عمل السيرفر المؤقت، فتصل الاستعادة إلى سيرفر يتوقف أو إلى مستخدم root لم تضبط كلمة مروره بعد، وتفشل بخطأ مثل the database system is shutting down أو Access denied. لذلك أضفنا قبل الاستعادة حلقة تنتظر حتى تقبل قاعدة البيانات الاتصال عبر الشبكة على 127.0.0.1، وهذا لا يحدث إلا بعد انتهاء التهيئة. ومع PostgreSQL تقرأ pg_restore الملف من الإدخال القياسي Standard Input، والخياران --clean --if-exists يحذفان أي كائن Object موجود قبل إنشائه:
docker compose up -d --wait db
until docker compose exec -T db pg_isready -h 127.0.0.1 -U app; do sleep 2; done
docker compose exec -T db pg_restore -U app -d app --clean --if-exists < migration/app.dumpومع MySQL، الأمر mysqladmin ping ينجح بمجرد أن يرد السيرفر حتى دون كلمة مرور، لذلك يكفي للانتظار:
docker compose up -d --wait db
until docker compose exec -T db mysqladmin ping -h 127.0.0.1 --silent; do sleep 2; done
docker compose exec -T db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" app' < migration/app.sqlوأخيراً شغل المشروع كاملاً، وتابع سجلاته:
docker compose up -d
docker compose logs -f --tail 100تحويل DNS إلى السيرفر الجديد
قبل أن تعدل DNS، اختبر التطبيقات على السيرفر الجديد مباشرة، حيث تستطيع أن تجعل جهازك وحده يرى النطاق على العنوان الجديد بسطر تضيفه إلى الملف /etc/hosts على جهازك (أو C:\Windows\System32\drivers\etc\hosts على Windows):
203.0.113.20 app.example.comفإذا عمل التطبيق كما يجب، فعدل سجل DNS من النوع A (وAAAA إن وجد) ليشير إلى عنوان السيرفر الجديد، ثم احذف السطر من الملف hosts. ولاحظ أنه إذا كان ال Reverse Proxy يصدر شهادات TLS من Let's Encrypt بالتحقق عبر HTTP (HTTP Challenge)، فلن ينجح التجديد على السيرفر الجديد قبل أن يشير النطاق إليه.
كيف تتأكد أن النقل نجح؟
قارن حالة السيرفر الجديد بما كان على القديم، ونفذ نفس الأوامر على السيرفرين إن أمكن:
docker ps -a --format 'table {{ .Names }}\t{{ .Status }}\t{{ .Image }}'
docker volume ls
docker image ls
docker network ls- يجب أن تظهر كل ال Containers بالحالة
Up، والتي تدعم فحص الصحة بالحالة(healthy). - يجب أن يتطابق عدد ال Volumes وال Images بين السيرفرين، وإذا ظهرت ال Volumes وغابت ال Images في الطريقة الأولى فارجع إلى فقرة اختفاء ال Images.
- اقرأ سجلات كل تطبيق وابحث عن أخطاء الصلاحيات Permissions أو الاتصال بقاعدة البيانات بالأمر
docker logs --tail 100 <name>. - جرب كل تطبيق كما يستخدمه الناس، أي سجل الدخول وافتح سجلاً قديماً وارفع ملفاً جديداً، وأرسل بريداً تجريبياً إن كان التطبيق يرسل بريداً.
- تأكد أنك أعدت ضبط المهام المجدولة Cron Jobs والنسخ الاحتياطية Backups التي كانت على السيرفر القديم، لأنها لا تنتقل مع Docker.
التراجع إذا فشل النقل
نحن لم نحذف ولم نعدل شيئاً على السيرفر القديم، لذلك فالتراجع Rollback سهل ما دام السيرفر القديم على حاله، فإذا ظهرت مشكلة لا تستطيع حلها خلال نافذة الصيانة، فأعد تشغيل Docker وال Containers التي أوقفتها، أو أعد تشغيل المشروع على السيرفر القديم:
sudo systemctl start containerd
sudo systemctl start docker
xargs -r docker start < running_containers.txtcd /opt/myapp
docker compose up -dوإذا كنت قد عدلت DNS فأعده إلى العنوان القديم، ولكن تذكر أن البيانات التي كتبها المستخدمون على السيرفر الجديد بعد التحويل لن تعود معك، لذلك قرر التراجع مبكراً، أو انقل تلك البيانات يدوياً.
مشكلات شائعة وحلولها
التطبيق لا يستطيع الكتابة بعد الاستعادة
سوف ترى في السجلات رسائل مثل Permission denied أو directory is not writable، والسبب في الغالب أن ملكية ملفات ال Volume تغيرت أثناء النقل، وهذا يحدث كثيراً عند فك أرشيف دون --numeric-owner أو عند نسخ الملفات بأداة لا تحفظ الملكية، حيث إن كثيراً من ال Images لا تعمل بحساب root وإنما بمستخدم له معرف UID محدد، وهذا المستخدم لا يستطيع الكتابة في ملفات لا يملكها. لذلك اعرف أولاً المعرف الذي يعمل به التطبيق، على السيرفر القديم إن أمكن:
docker compose exec app idوالمخرج سوف يكون كما يلي:
uid=1000(app) gid=1000(app) groups=1000(app)ثم أعد ملكية ال Volume إلى هذا المعرف عبر Container مؤقت، فلا تحتاج إلى معرفة مسار ال Volume على القرص:
docker compose stop app
docker run --rm -v myapp_app-data:/data ubuntu:24.04 chown -R 1000:1000 /data
docker compose start appولا تلجأ إلى الحل السريع chmod -R 777، والسبب أنه يخفي المشكلة ولكنه يعطي كل مستخدم على السيرفر صلاحية الكتابة في بيانات التطبيق.
اختفاء ال Images وال Containers بعد النسخ الكامل
الخدمة تعمل والأمر docker volume ls يعرض ال Volumes، ولكن docker ps -a وdocker image ls فارغان، والسبب هو اختلاف طريقة التخزين بين السيرفرين كما شرحنا في فقرة طريقة التخزين، فالبيانات موجودة على القرص ولكن Docker يبحث عنها في مكان آخر. لذلك نفذ docker info -f '{{ .Driver }} {{ .DriverStatus }}' على السيرفرين، واضبط الخيار containerd-snapshotter في /etc/docker/daemon.json على السيرفر الجديد ليطابق القديم، ثم أعد تشغيل Docker.
Docker لا يبدأ بعد فك الأرشيف
اقرأ سجل الخدمة أولاً:
sudo journalctl -u docker -n 50 --no-pagerوأشهر الأسباب ثلاثة: إصدار Docker على السيرفر الجديد أقدم من القديم، أو خطأ في صياغة daemon.json، أو قرص امتلأ أثناء فك الأرشيف، وتتحقق من ذلك بالأمر df -h. وإذا لم تجد الحل فأوقف Docker، واحذف المجلد المستعاد، وأعد المجلد /var/lib/docker.fresh إلى مكانه، ثم انتقل إلى الطريقة الثانية.
رسالة Cannot stat: No such file or directory أثناء إنشاء الأرشيف
معنى الرسالة أن أحد المسارات في host_paths.txt لم يعد موجوداً، وفي الغالب هو Container متوقف منذ زمن ويشير إلى مجلد حذف، وtar يكمل الأرشيف دون هذا المسار، ولكن راجع القائمة حتى تتأكد أن المسار المفقود لا يخص تطبيقاً تحتاجه.
رسالة port is already allocated على السيرفر الجديد
هذا يعني أن على السيرفر الجديد خدمة تستخدم نفس المنفذ Port، وكثيراً ما تكون سيرفر ويب Web Server ثبته المزود مسبقاً، فاعرف الخدمة بالأمر sudo ss -ltnp، ثم أوقفها أو عطلها قبل تشغيل ال Containers.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- افحص الإصدار والمعمارية وطريقة التخزين على السيرفرين قبل النقل، فالتثبيت الجديد من Docker 29 يستخدم containerd، والسيرفر الذي قمت بتحديثه يبقى على
overlay2. - نسخ
/var/lib/dockerوحده لا ينقل ال Bind Mounts، لذلك اجمع مصادرها بالأمرdocker inspectقبل إيقاف Docker. - أوقف ال Containers قبل الخدمة لأن
live-restoreيبقيها عاملة، واحفظ أسماءها حتى تشغلها على السيرفر الجديد. - استخدم مع tar الخيارات
--numeric-owner --xattrs --xattrs-include='*' --acls، ومع rsync الخيارات-aHAX -r --numeric-ids، وإلا فسوف تضيع الملكية والسمات التي يعتمد عليها Docker. - في الطريقة الثانية صدر قاعدة البيانات إلى ملف بدل نسخ ملفاتها، وانتظر حتى تقبل الاتصال على
127.0.0.1قبل أن تستعيدها. - لا تحذف شيئاً من السيرفر القديم حتى تمر دورة عمل كاملة وتنجح أول نسخة احتياطية على الجديد.
سجل التحديثات
- أكتوبر 2026: كتابة الدليل.