> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# نقل Containers إلى خادم جديد دون فقدان البيانات
- URL: https://arabroot.io/articles/نقل-containers-إلى-خادم-جديد/
- Published: 2026-09-28T15:05:00.000Z
- Updated: 2026-10-05T06:20:47.000Z
- Description: عندما تنتقل إلى سيرفر جديد أو مزود آخر، تريد أن تصل ال Containers مع بياناتها كما هي. نشرح طريقتين للنقل، نسخ كل شيء دفعة واحدة أو نقل كل تطبيق على حدة، مع خطة للتراجع إذا تعثر النقل.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, Docker والحاويات, التشغيل والنسخ الاحتياطي, Docker

عندما يحين وقت الانتقال إلى سيرفر 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](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/) يشرح التثبيت من المستودع الرسمي، أما دليل [تأمين خادم VPS من أول دخول](https://arabroot.io/articles/%D8%AA%D8%A3%D9%85%D9%8A%D9%86-%D8%AE%D8%A7%D8%AF%D9%85-vps-%D9%85%D9%86-%D8%A3%D9%88%D9%84-%D8%AF%D8%AE%D9%88%D9%84/) فيشرح ما يجب أن تفعله قبل أن تنقل إليه أي بيانات.
- مستخدم يملك صلاحية `sudo` على السيرفرين، ودخول عبر SSH من السيرفر القديم إلى الجديد، وسوف نستخدم في هذا الدليل المستخدم `deploy` والعنوان التوضيحي `new-server.example.com` (أو `203.0.113.20`).
- مساحة كافية على القرص Disk، أي مكان للأرشيف Archive على السيرفر القديم، ومكان للأرشيف وللبيانات بعد فكه على الجديد.
- نافذة صيانة Maintenance Window يقبل فيها المستخدمون توقف التطبيقات، وصلاحية تعديل سجلات DNS للنطاقات Domains التي تشير إلى السيرفر.

## افحص السيرفرين قبل أن تبدأ

أغلب حالات الفشل في النسخ الكامل لا تظهر إلا بعد فك الأرشيف، فإما أن تختفي ال Images ظاهرياً أو يرفض Docker أن يبدأ، لذلك قارن السيرفرين أولاً بتنفيذ هذه الأوامر على كل منهما:

```bash
docker version -f '{{ .Server.Version }}'
docker info -f '{{ .Architecture }}'
docker info -f '{{ .DockerRootDir }}'
docker info -f '{{ .Driver }} {{ .DriverStatus }}'
```

في الأوامر أعلاه لاحظ التالي:

- **الإصدار Version**: يجب أن يكون Docker Engine على السيرفر الجديد بنفس الإصدار أو أحدث منه، ولا تنقل البيانات أبداً إلى إصدار أقدم، والسبب أن الإصدار القديم لا يضمن أن يفهم ملفات كتبها إصدار أحدث، وإذا أردت نفس الإصدار تماماً فصفحة [التثبيت على Ubuntu](https://docs.docker.com/engine/install/ubuntu/?ref=arabroot.io) تشرح تثبيت إصدار محدد بالأمر `apt list --all-versions docker-ce`.
- **المعمارية Architecture**: يجب أن تتطابق، أي `x86_64` مع `x86_64` أو `aarch64` مع `aarch64`، والسبب أن ال Image المحفوظة على القرص [مبنية لمعمارية واحدة](https://docs.docker.com/build/building/multi-platform/?ref=arabroot.io)، فلن تعمل على معالج من نوع آخر.
- **مجلد البيانات Data Root**: القيمة المعتادة هي `/var/lib/docker`، فإذا ظهر مسار آخر فهذا يعني أن أحدهم غيره بالخيار `data-root`، وعليك أن تضعه مكان `/var/lib/docker` في كل أوامر هذا الدليل.
- **طريقة التخزين Storage Backend**: وهذه النقطة يغفل عنها كثيرون، لذلك نشرحها في الفقرة التالية.

### لماذا يجب أن تتطابق طريقة التخزين؟

منذ الإصدار 29.0 يستخدم Docker Engine في التثبيت الجديد [مخزن ال Images الخاص بـ containerd](https://docs.docker.com/engine/storage/containerd/?ref=arabroot.io) وليس مشغل التخزين التقليدي Storage Driver `overlay2`، ولكن السيرفر الذي قمت بتحديثه Upgrade من إصدار أقدم يبقى على `overlay2`، والمشكلة أن مكان البيانات يختلف بين الحالتين كما تشرح [وثائق Docker](https://docs.docker.com/engine/daemon/?ref=arabroot.io#daemon-data-directory)، فمع `overlay2` تكون كل البيانات في `/var/lib/docker`، أما مع containerd فتكون ال Images وطبقات Layers ال Containers في `/var/lib/containerd`، وتبقى ال Volumes والإعدادات في `/var/lib/docker`.

لذلك انظر في مخرج الأمر الأخير، فإذا ظهر فيه `io.containerd.snapshotter.v1` فالسيرفر يستخدم containerd، وإلا فهو على `overlay2` ويظهر اسمه في بداية السطر. ولاحظ أن الوثائق تذكر أن التبديل بين الطريقتين [يخفي ال Images وال Containers](https://docs.docker.com/engine/storage/containerd/?ref=arabroot.io) التي أنشئت بالطريقة الأخرى، فتبدو وكأنها حذفت، وهي في الحقيقة ما زالت على القرص.

وبالتالي إذا كان السيرفر القديم على `overlay2` والجديد تثبيتاً حديثاً بالإصدار 29، فعليك أن توقف خيار containerd على السيرفر الجديد [في ملف daemon.json](https://docs.docker.com/reference/cli/dockerd/?ref=arabroot.io) قبل النقل، وإذا كان الملف موجوداً فأضف إليه المفتاح `features` ولا تستبدله:

```bash
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` الذي أضفته للتو.

### كم مساحة تحتاج، وكم سيتوقف التطبيق؟

احسب حجم البيانات على السيرفر القديم، والمساحة المتاحة على السيرفرين، كما يلي:

```bash
docker system df
sudo du -sh /var/lib/docker /var/lib/containerd
df -h /
```

وهذا الحجم هو الذي يحدد مدة التوقف تقريباً، لأن وقت إنشاء الأرشيف ونقله وفكه يزيد معه، وإذا كانت التطبيقات مهمة فقم بتخفيض مدة صلاحية سجلات DNS واختصاراً TTL إلى 300 ثانية قبل النقل بيوم، وبهذا الشكل يصل التحويل إلى السيرفر الجديد خلال دقائق وليس خلال ساعات، والسبب شرحناه في [شرح DNS وسجلاته للمبتدئين](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

## الطريقة الأولى: نسخ بيانات 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](https://docs.docker.com/reference/cli/docker/inspect/?ref=arabroot.io) يعرض مصدر كل ربط، لذلك اجمع هذه المصادر **قبل** إيقاف Docker، لأن الأمر يحتاج إلى الخدمة وهي تعمل:

```bash
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:

```bash
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](https://docs.docker.com/engine/daemon/live-restore/?ref=arabroot.io)، ومع هذا الخيار تبقى ال Containers عاملة وتكتب في ال Volumes حتى بعد أن تتوقف خدمة Docker، وبالتالي سوف تنسخ قاعدة بيانات تتغير أثناء النسخ. لذلك احفظ أسماء ال Containers التي تعمل الآن في ملف، ثم أوقفها، ثم انقل الملف إلى السيرفر الجديد لأنك سوف تحتاجه هناك:

```bash
docker info -f '{{ .LiveRestoreEnabled }}'
docker ps --format '{{ .Names }}' > running_containers.txt
xargs -r docker stop < running_containers.txt
rsync -a running_containers.txt deploy@new-server.example.com:/home/deploy/
```

الآن أوقف المقبس `docker.socket` مع الخدمة، وإلا فسوف يعيد systemd تشغيل Docker عند أول أمر يصله، ثم أوقف containerd أيضاً حتى لا تتغير ملفاته أثناء النسخ:

```bash
sudo systemctl stop docker.socket docker
sudo systemctl stop containerd
systemctl is-active docker containerd
```

ويجب أن تظهر الكلمة `inactive` مرتين.

### إنشاء الأرشيف

```bash
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` [يقرأ المسارات من الملف](https://www.gnu.org/software/tar/manual/html%5Fnode/files.html?ref=arabroot.io) سطراً سطراً، وبالتالي لا تنكسر المسارات التي فيها مسافات، كما كان سوف يحدث لو كتبت `$(cat host_paths.txt)`.
- `--numeric-owner` [يحفظ أرقام المالكين](https://www.gnu.org/software/tar/manual/html%5Fnode/Attributes.html?ref=arabroot.io) وليس أسماءهم، والسبب أن المستخدم `postgres` مثلاً قد يحمل رقماً آخر على السيرفر الجديد أو لا يكون موجوداً عليه أصلاً، أما التطبيقات داخل ال Containers فتعتمد على الرقم وحده.
- `--xattrs --xattrs-include='*'` يحفظ السمات الموسعة، حيث يعتمد `overlay2` على السمة `trusted.overlay.opaque` [في نواة Linux Kernel](https://docs.kernel.org/filesystems/overlayfs.html?ref=arabroot.io) ليعرف [المجلدات التي حذفت داخل ال Container](https://docs.docker.com/engine/storage/drivers/overlayfs-driver/?ref=arabroot.io)، ولكن tar عند فك الأرشيف لا يستعيد افتراضياً [إلا سمات النوع user](https://www.gnu.org/software/tar/manual/html%5Fnode/Extended-File-Attributes.html?ref=arabroot.io)، لذلك نطلب كل الأنواع صراحة، وإذا نسيت هذا الخيار فسوف تفقد هذه السمة بصمت دون أي رسالة خطأ.
- `--acls` يحفظ قوائم التحكم بالوصول Access Control Lists واختصاراً ACLs إن وجدت.

وسوف تظهر لك الرسالة `` Removing leading `/' from member names ``، وهذا طبيعي لأن tar يحفظ المسارات نسبية، ونحن سوف نفكها لاحقاً في `/`. بعد ذلك سجل بصمة الأرشيف Checksum حتى تقارنها بعد النقل:

```bash
sha256sum ~/full_backup.tar.gz
```

🛑

يحتوي الأرشيف على كل أسرار تطبيقاتك، أي ملفات `.env` وكلمات مرور قواعد البيانات والمفاتيح الخاصة Private Keys لشهادات TLS، ولذلك قصرنا صلاحية قراءته على مالكه بالأمر `chmod 600`، فلا تضعه في مجلد مشترك، واحذفه من السيرفرين بعد انتهاء النقل والتحقق.

### نقل الأرشيف إلى السيرفر الجديد

```bash
rsync -a --partial --progress ~/full_backup.tar.gz deploy@new-server.example.com:/home/deploy/
```

الخيار `--partial` [يحتفظ بالجزء الذي وصل](https://download.samba.org/pub/rsync/rsync.1?ref=arabroot.io) إذا انقطع الاتصال، فتستأنف النقل بدل أن تبدأ من الصفر، وهذا مهم مع أرشيف بعشرات الجيجابايت. أما الخيار `-z` فلا فائدة منه هنا، لأن الأرشيف مضغوط أصلاً. وعلى السيرفر الجديد تأكد أن البصمة تطابق ما سجلته:

```bash
sha256sum /home/deploy/full_backup.tar.gz
```

### بديل: النسخ المباشر بـ rsync دون أرشيف

إذا لم يكن على السيرفر القديم مكان لأرشيف بحجم البيانات، فانسخ المجلدات مباشرة إلى السيرفر الجديد. وقد يتساءل البعض: لماذا لا نكتفي بالأمر المعروف `rsync -a`؟ والإجابة أن الخيار `-a` وحده [لا يشمل](https://download.samba.org/pub/rsync/rsync.1?ref=arabroot.io) الروابط الصلبة ولا قوائم 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 نفذ الأمر التالي:

```bash
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 / deploy@new-server.example.com:/
```

ولاحظ أن الأمر يعمل بصلاحيات `sudo`، لذلك سوف يسألك ssh عند أول اتصال عن بصمة السيرفر الجديد حتى لو كنت قد قبلتها من قبل بمستخدمك العادي. وقبل تنفيذه أوقف Docker على السيرفر الجديد وانقل مجلداته الحديثة جانباً كما في الخطوة التالية، وعندما ينتهي النقل احذف ملف sudoers والمفتاح المؤقت فوراً.

### الاستعادة على السيرفر الجديد

أوقف Docker على السيرفر الجديد، وانقل جانباً المجلدات التي أنشأها التثبيت الحديث بدل أن تفك الأرشيف فوقها، والسبب أنك بهذا الشكل لا تخلط بين حالتين مختلفتين، وتبقى عندك نسخة نظيفة تعود إليها إذا احتجت:

```bash
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
```

ثم فك الأرشيف في `/` بنفس الخيارات، فيرجع كل مجلد إلى مساره الأصلي:

```bash
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`، ثم شغل الخدمتين:

```bash
sudo systemctl start containerd
sudo systemctl start docker
docker ps -a
```

ال Containers التي لها [سياسة إعادة التشغيل Restart Policy](https://docs.docker.com/engine/containers/start-containers-automatically/?ref=arabroot.io) بالقيمة `always` تبدأ تلقائياً مع Docker، أما التي لها القيمة `unless-stopped` فلن تبدأ، والسبب أننا أوقفناها يدوياً قبل النسخ، والوثائق تذكر أن هذه السياسة لا تعيد تشغيل Container أوقفته بنفسك حتى بعد إعادة تشغيل Docker. لذلك شغل كل ما كان يعمل على السيرفر القديم من الملف الذي نقلته:

```bash
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`، فنفذ الخطوات التالية على السيرفر القديم داخل مجلد المشروع:

```bash
cd /opt/myapp
mkdir -p migration
docker volume ls --filter label=com.docker.compose.project=myapp
```

### قاعدة البيانات: صدرها إلى ملف أولاً

أمامك خياران مع قاعدة البيانات، إما أن تنسخ ملفاتها من ال Volume، أو أن تفرغها إلى ملف Database Dump ثم تستعيدها، والتفريغ هو الأسلم عند النقل، والسبب أنه لا يعتمد على تطابق ملفات قاعدة البيانات بين السيرفرين، ويكشف لك أي تلف قبل أن تحذف شيئاً. لذلك أوقف التطبيق أولاً حتى لا يكتب بيانات جديدة أثناء التفريغ، واترك قاعدة البيانات تعمل.

مع PostgreSQL، الخيار `-Fc` يحفظ التفريغ [بصيغة مخصصة Custom Format](https://www.postgresql.org/docs/current/app-pgdump.html?ref=arabroot.io) تقرؤها الأداة `pg_restore`:

```bash
docker compose stop app
docker compose exec -T db pg_dump -U app -d app -Fc > migration/app.dump
```

ومع MySQL، الخيار `--single-transaction` يأخذ [نسخة متسقة Consistent لجداول InnoDB](https://dev.mysql.com/doc/refman/8.4/en/mysqldump.html?ref=arabroot.io) دون أن يقفلها Lock، ونقرأ كلمة المرور من متغيرات البيئة Environment Variables داخل ال Container حتى لا تظهر في سجل الأوامر Shell History:

```bash
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](https://mariadb.com/kb/en/mariadb-dump/?ref=arabroot.io) و`mariadb` بنفس الخيارات.

تأكد أن الملف ليس فارغاً بالأمر `ls -lh migration/`، ثم أوقف المشروع كاملاً، واستخدم `stop` وليس `down -v`، والسبب أن الخيار `-v` يحذف ال Volumes، وأنت تحتاجها إذا قررت التراجع:

```bash
docker compose stop
```

### نسخ ال Volumes الأخرى

ال Volumes التي ليس فيها قاعدة بيانات، مثل الملفات المرفوعة، لها طريقة بسيطة في [وثائق Docker](https://docs.docker.com/engine/storage/volumes/?ref=arabroot.io#back-up-restore-or-migrate-data-volumes)، حيث تشغل Container مؤقتاً يربط ال Volume للقراءة فقط ويربط مجلداً على السيرفر، ثم يضع محتوى ال Volume في أرشيف داخل ذلك المجلد:

```bash
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` بما فيه من تفريغ وأرشيفات، لذلك ضعه في أرشيف واحد مع حفظ الملكية ثم انقله:

```bash
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 deploy@new-server.example.com:/home/deploy/
```

### إعادة بناء المشروع على السيرفر الجديد

فك الأرشيف، ثم اطلب من Compose أن [ينشئ ال Containers وال Volumes](https://docs.docker.com/reference/cli/docker/compose/create/?ref=arabroot.io) دون أن يشغلها، وبهذا الشكل تنشأ ال Volumes بالأسماء والوسوم Labels التي يتوقعها Compose، ثم تملؤها أنت بالبيانات:

```bash
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](https://docs.docker.com/engine/storage/volumes/?ref=arabroot.io#restore-volume-from-a-backup) بنفس الطريقة ولكن في الاتجاه المعاكس:

```bash
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](https://docs.docker.com/reference/cli/docker/compose/up/?ref=arabroot.io)، ولكن لا تعتمد على هذا الخيار وحده هنا، والسبب أن ال Volume جديد وفارغ، فتبدأ ال Image الرسمية لـ [PostgreSQL](https://hub.docker.com/%5F/postgres?ref=arabroot.io) و[MySQL](https://hub.docker.com/%5F/mysql?ref=arabroot.io) بتهيئة قاعدة البيانات عبر سيرفر مؤقت لا يقبل إلا الاتصال المحلي عبر ال Socket، ثم توقفه وتشغل السيرفر الحقيقي، وقد ينجح فحص الصحة Health Check أثناء عمل السيرفر المؤقت، فتصل الاستعادة إلى سيرفر يتوقف أو إلى مستخدم root لم تضبط كلمة مروره بعد، وتفشل بخطأ مثل `the database system is shutting down` أو `Access denied`. لذلك أضفنا قبل الاستعادة حلقة تنتظر حتى تقبل قاعدة البيانات الاتصال عبر الشبكة على `127.0.0.1`، وهذا لا يحدث إلا بعد انتهاء التهيئة. ومع PostgreSQL تقرأ `pg_restore` الملف [من الإدخال القياسي Standard Input](https://www.postgresql.org/docs/current/app-pgrestore.html?ref=arabroot.io)، والخياران `--clean --if-exists` يحذفان أي كائن Object موجود قبل إنشائه:

```bash
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` ينجح بمجرد أن يرد السيرفر حتى دون كلمة مرور، لذلك يكفي للانتظار:

```bash
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
```

وأخيراً شغل المشروع كاملاً، وتابع سجلاته:

```bash
docker compose up -d
docker compose logs -f --tail 100
```

## تحويل DNS إلى السيرفر الجديد

قبل أن تعدل DNS، اختبر التطبيقات على السيرفر الجديد مباشرة، حيث تستطيع أن تجعل جهازك وحده يرى النطاق على العنوان الجديد بسطر تضيفه إلى الملف `/etc/hosts` على جهازك (أو `C:\Windows\System32\drivers\etc\hosts` على Windows):

```bash
203.0.113.20 app.example.com
```

فإذا عمل التطبيق كما يجب، فعدل سجل DNS من النوع A (وAAAA إن وجد) ليشير إلى عنوان السيرفر الجديد، ثم احذف السطر من الملف `hosts`. ولاحظ أنه إذا كان ال Reverse Proxy يصدر شهادات TLS من Let's Encrypt بالتحقق عبر HTTP (HTTP Challenge)، فلن ينجح التجديد على السيرفر الجديد قبل أن يشير النطاق إليه.

## كيف تتأكد أن النقل نجح؟

قارن حالة السيرفر الجديد بما كان على القديم، ونفذ نفس الأوامر على السيرفرين إن أمكن:

```bash
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](#images-missing).
- اقرأ سجلات كل تطبيق وابحث عن أخطاء الصلاحيات Permissions أو الاتصال بقاعدة البيانات بالأمر `docker logs --tail 100 <name>`.
- جرب كل تطبيق كما يستخدمه الناس، أي سجل الدخول وافتح سجلاً قديماً وارفع ملفاً جديداً، وأرسل بريداً تجريبياً إن كان التطبيق يرسل بريداً.
- تأكد أنك أعدت ضبط المهام المجدولة Cron Jobs والنسخ الاحتياطية Backups التي كانت على السيرفر القديم، لأنها لا تنتقل مع Docker.

## التراجع إذا فشل النقل

نحن لم نحذف ولم نعدل شيئاً على السيرفر القديم، لذلك فالتراجع Rollback سهل ما دام السيرفر القديم على حاله، فإذا ظهرت مشكلة لا تستطيع حلها خلال نافذة الصيانة، فأعد تشغيل Docker وال Containers التي أوقفتها، أو أعد تشغيل المشروع على السيرفر القديم:

```bash
sudo systemctl start containerd
sudo systemctl start docker
xargs -r docker start < running_containers.txt
```

```bash
cd /opt/myapp
docker compose up -d
```

وإذا كنت قد عدلت DNS فأعده إلى العنوان القديم، ولكن تذكر أن البيانات التي كتبها المستخدمون على السيرفر الجديد بعد التحويل لن تعود معك، لذلك قرر التراجع مبكراً، أو انقل تلك البيانات يدوياً.

💡

في أبريل 2018 نقل بنك TSB البريطاني بيانات عملائه إلى منصة جديدة، ووصلت البيانات نفسها سليمة، ولكن المنصة الجديدة تعطلت مباشرة بعد التحويل، فتأثرت كل الفروع ونسبة كبيرة من عملائه البالغ عددهم 5.2 مليون، ولم تعد الخدمة إلى وضعها الطبيعي إلا في ديسمبر 2018، وفي 2022 [غرمته الجهات الرقابية 48.65 مليون جنيه](https://www.fca.org.uk/news/press-releases/tsb-fined-48m-operational-resilience-failings?ref=arabroot.io) لأنه لم يخطط للترحيل كما يجب. والدرس هنا أن تختبر التطبيق نفسه وليس البيانات وحدها قبل التحويل، وأن تبقي طريق الرجوع مفتوحاً حتى تتأكد.

⚠️

أبق السيرفر القديم وأرشيفات النقل أياماً بعد التحويل، حتى تمر دورة عمل كاملة تتضمن المهام الأسبوعية والتقارير، والسبب أن بعض الأعطال لا يظهر إلا مع أول مهمة تعمل ليلاً، ولا تلغ السيرفر القديم عند المزود قبل أن تتأكد من نجاح أول نسخة احتياطية على السيرفر الجديد.

## مشكلات شائعة وحلولها

### التطبيق لا يستطيع الكتابة بعد الاستعادة

سوف ترى في السجلات رسائل مثل `Permission denied` أو `directory is not writable`، والسبب في الغالب أن ملكية ملفات ال Volume تغيرت أثناء النقل، وهذا يحدث كثيراً عند فك أرشيف دون `--numeric-owner` أو عند نسخ الملفات بأداة لا تحفظ الملكية، حيث إن كثيراً من ال Images لا تعمل بحساب root وإنما بمستخدم له معرف UID محدد، وهذا المستخدم لا يستطيع الكتابة في ملفات لا يملكها. لذلك اعرف أولاً المعرف الذي يعمل به التطبيق، على السيرفر القديم إن أمكن:

```bash
docker compose exec app id
```

والمخرج سوف يكون كما يلي:

```bash
uid=1000(app) gid=1000(app) groups=1000(app)
```

ثم أعد ملكية ال Volume إلى هذا المعرف عبر Container مؤقت، فلا تحتاج إلى معرفة مسار ال Volume على القرص:

```bash
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` فارغان، والسبب هو اختلاف طريقة التخزين بين السيرفرين كما شرحنا في [فقرة طريقة التخزين](#storage-backend)، فالبيانات موجودة على القرص ولكن Docker يبحث عنها في مكان آخر. لذلك نفذ `docker info -f '{{ .Driver }} {{ .DriverStatus }}'` على السيرفرين، واضبط الخيار `containerd-snapshotter` في `/etc/docker/daemon.json` على السيرفر الجديد ليطابق القديم، ثم أعد تشغيل Docker.

### Docker لا يبدأ بعد فك الأرشيف

اقرأ سجل الخدمة أولاً:

```bash
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: كتابة الدليل.