الأدلة التقنية

تثبيت WordPress مع MariaDB وDocker Compose

شغل موقع WordPress على خادمك بإعداد يصلح للاستخدام الفعلي، لا للتجربة فقط. نثبته مع MariaDB عبر Docker Compose، ونفعل HTTPS، ونجهز نسخاً احتياطياً يمكنك استعادته عند الحاجة.

تثبيت WordPress مع MariaDB وDocker Compose

إذا أردت أن تطلق مدونة أو موقعاً لشركتك على WordPress، فالحل الأول الذي يخطر على البال هو أن تشترك في استضافة مشتركة Shared Hosting وتضغط زر التثبيت بنقرة واحدة One-Click Install من لوحة التحكم، وهذا الحل يعمل في اليوم الأول، ولكنك سوف تجد مع الوقت أنه يعيش على سيرفر Server يتقاسمه معك عشرات العملاء، فلا تختار فيه إصدار PHP ولا حدود الرفع ولا موعد التحديث، وإذا أبطأ موقع جارك أبطأ موقعك معه، والنسخ الاحتياطي Backup فيه عادة نسخة تحتفظ بها الشركة لأيام قليلة ولم تجرب أنت استعادتها في أي يوم. والحل الثاني هو الاستضافة المدارة Managed Hosting المخصصة لـ WordPress، وهي حل جيد ومريح، ولكنك تدفع فيها عن كل موقع، وتقيدك أحياناً في الإضافات Plugins التي تسمح بها، وتبقى بياناتك على سيرفرات شركة أخرى.

لذلك فالحل الأنسب إذا كان عندك سيرفر تشغل عليه خدماتك الأخرى هو أن تستضيف WordPress بنفسك عبر Docker Compose، فتتحكم في الإضافات والبيانات والتكلفة، وتشغل الموقع بجانب بقية خدماتك على السيرفر نفسه، ولكن تذكر أن ثمن ذلك هو أن التحديثات Updates والأمان Security والنسخ الاحتياطي تصبح مسؤوليتك أنت، فإذا كان الموقع مصدر دخل أساسياً ولم يكن في فريقك من يتابعه، فقد تكون الاستضافة المدارة أجدى لك من الناحية الاقتصادية.

والتثبيت بـ Docker نفسه يستغرق دقائق، ولكن كثيراً من الأمثلة المنشورة لا يصلح لبيئة الإنتاج Production، فسوف تجد فيها كلمة مرور Password مثل «TheDbPassword»، وImage بالوسم Tag latest يتغير محتواها دون علمك، وحد رفع Upload Limit لا يتجاوز 2 ميجابايت، ثم يدخل HTTPS خلف الـ Reverse Proxy في حلقة تحويل Redirect Loop لا تنتهي، والسبب أن هذه الأمثلة كتبت لتعمل على جهاز المطور وليس لتبقى سنوات على سيرفر حقيقي.

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

وسوف نناقش في هذا المقال ما يلي:

  • تثبيت WordPress مع MariaDB عبر Docker Compose بكلمات مرور مولدة وإصدارات محددة بأرقامها وفحوصات صحة Health Checks، وشرح كل سطر مهم في الملف.
  • رفع حدود الرفع في PHP، وتشغيل HTTPS خلف الـ Reverse Proxy دون حلقة تحويل.
  • معالج التثبيت والروابط الدائمة وصحة الموقع، ثم الإدارة من الطرفية بـ wp-cli.
  • النسخ الاحتياطي لقاعدة البيانات وwp-content والاستعادة على سيرفر جديد، ثم التحديث وأشهر المشكلات وحلولها.

ما تحتاجه قبل أن تبدأ Requirements

  • سيرفر مثبت عليه Docker وCompose، وإذا لم تثبته بعد فابدأ بدليل تثبيت Docker على Ubuntu.
  • الموارد Resources: دون أي زيارات يستهلك الـ Container الخاص بـ WordPress قرابة 160 ميجابايت من الذاكرة RAM، وتستهلك MariaDB قرابة 145 ميجابايت، أما الموقع الفعلي فخطط له بذاكرة من 1 إلى 2 GB، وبمساحة تتسع للوسائط Media والنسخ الاحتياطية، والسبب أن الوسائط هي التي تكبر مع الوقت وليس قاعدة البيانات.
  • نطاق Domain مثل blog.example.com، وReverse Proxy يعمل بـ HTTPS مثل Nginx Proxy Manager ومتصل بشبكة Docker باسم proxy.
  • لا تحتاج إلى فتح أي منفذ Port على السيرفر سوى 80 و443 للـ Reverse Proxy.

تثبيت WordPress بـ Docker Compose

مجلد التثبيت والأسرار Secrets

نبدأ بإنشاء مجلد للخدمة يحفظ فيه ملف Compose وملف الأسرار وملف إعدادات PHP:

sudo mkdir -p /opt/wordpress
sudo chown $USER: /opt/wordpress
cd /opt/wordpress

بعد ذلك نولد كلمتي مرور قاعدة البيانات Database عشوائياً في ملف .env لا يقرؤه أحد غيرك، والسبب أننا لا نريد أن نكتب كلمة المرور داخل ملف Compose نفسه فتظهر في كل نسخة منه:

cat > .env <<EOF
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 32)
MARIADB_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 .env

الآن سوف ننشئ ملف إعدادات PHP الخاص بالرفع، وهو /opt/wordpress/uploads.ini، والسبب أن القيم الافتراضية في الـ Image تسمح بـ 2 ميجابايت فقط للملف الواحد، فتظهر للمحرر رسالة «الملف المراد رفعه تجاوز الحد الأقصى» مع أول صورة كبيرة يرفعها:

upload_max_filesize = 256M
post_max_size = 260M
memory_limit = 512M
max_execution_time = 300
max_input_time = 300

في الإعداد أعلاه لاحظ أن post_max_size أكبر قليلاً من upload_max_filesize، والسبب أن الأول حد للطلب كله، والطلب يحمل مع الملف حقول النموذج، فإذا تساوى الحدان رفض PHP الملف الذي يقترب من الحد دون أي رسالة واضحة، وأن memory_limit أكبر من الاثنين كما يوصي التوثيق. وتجد الفرق بين هذه الحدود وكيف تعرف أي طبقة رفضت الملف في دليل حدود حجم رفع الملفات. وهذه الطريقة أنظف من تعديل .htaccess داخل مجلد الموقع، فالملف يبقى خارج البيانات، ويبقى في Git مع بقية الإعداد، ولا يمحوه أي تحديث.

ملف Compose وشرح ما فيه

الملف /opt/wordpress/compose.yaml سوف يكون كما يلي:

services:
  db:
    image: mariadb:11.8.9
    container_name: wordpress-db
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: ${MARIADB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MARIADB_AUTO_UPGRADE: "1"
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - internal
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  wordpress:
    image: wordpress:7.1.2-php8.4-apache
    container_name: wordpress
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD}
      WORDPRESS_CONFIG_EXTRA: |
        define('WP_HOME', 'https://blog.example.com');
        define('WP_SITEURL', 'https://blog.example.com');
        define('FORCE_SSL_ADMIN', true);
        define('FS_METHOD', 'direct');
        define('WP_MEMORY_LIMIT', '256M');
        define('DISALLOW_FILE_EDIT', true);
    volumes:
      - wp_data:/var/www/html
      - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
    networks:
      - internal
      - proxy
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS -o /dev/null http://localhost/wp-includes/images/blank.gif || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3

  wpcli:
    image: wordpress:cli-2.12.0-php8.4
    profiles: ["cli"]
    user: "33:33"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD}
      HOME: /tmp
    volumes:
      - wp_data:/var/www/html
    networks:
      - internal
    depends_on:
      - db

volumes:
  db_data:
    name: wordpress_db
  wp_data:
    name: wordpress_html

networks:
  internal: {}
  proxy:
    external: true

في الإعداد أعلاه لاحظ التالي:

  • MariaDB 11.8 إصدار دعم طويل LTS تصله الإصلاحات حتى يونيو 2028، وقد صدر بعده الإصدار 12.3 LTS في مايو 2026، ونبقى هنا على 11.8 لأنه مستقر منذ أكثر من سنة وأمامه قرابة سنتين من الدعم، والانتقال بين الفرعين لاحقاً خطوة مستقلة تأخذ لها نسخة احتياطية. والمتغير MARIADB_AUTO_UPGRADE يرقي جداول النظام System Tables تلقائياً عندما تغير الوسم إلى إصدار أحدث.
  • فحص صحة القاعدة Health Check يستخدم السكربت المدمج في الـ Image، والشرط condition: service_healthy يؤخر تشغيل WordPress حتى تبدأ القاعدة بقبول الاتصالات، وبالتالي لا تظهر لك رسالة «Error establishing a database connection» عند الإقلاع Startup. وفحص WordPress نفسه يطلب ملفاً ثابتاً صغيراً بـ curl الموجود في الـ Image، فيعرف أن Apache يرد دون أن يحمل صفحة كاملة كل 30 ثانية.
  • لا توجد منافذ منشورة على السيرفر، فالـ Container الخاص بـ WordPress على شبكة proxy ويصل إليه الـ Reverse Proxy بالاسم wordpress، والقاعدة على شبكة internal وحدها، فلا يصل إليها أحد من خارج الخدمة.
  • WORDPRESS_CONFIG_EXTRA تضيف الـ Image محتواه إلى ملف wp-config.php، والثابتان WP_HOME وWP_SITEURL يثبتان الرابط العام للموقع حتى لا يسجل WordPress في القاعدة عنواناً خاطئاً، والقيمة direct في FS_METHOD تمنعه من طلب بيانات FTP عند تثبيت الإضافات وتحديثها، أما DISALLOW_FILE_EDIT فيعطل محرر ملفات PHP في لوحة التحكم Dashboard، والسبب أن هذا المحرر هو أول ما يستغله من يستولي على حساب مدير Administrator ليزرع كوده في الموقع.
  • wpcli خدمة مقيدة بـ profiles، فلا تعمل مع up وإنما تستدعيها عند الحاجة، ولاحظ أن الـ Image الخاصة بها مبنية على Alpine والمستخدم www-data فيها رقمه 82، بينما مالك ملفات الموقع في Image الـ Apache هو المستخدم 33، لذلك نشغلها بالمستخدم 33 حتى لا تفسد الصلاحيات Permissions، ولأن هذا المستخدم ليس له مجلد خاص فيها نضع HOME: /tmp، وإلا ظهر لك مع أوامر التحميل التحذير Failed to create directory '/.wp-cli/cache/'.
💡
إذا أردت أن تجرب محلياً قبل ضبط النطاق، فاحذف أسطر WP_HOME وWP_SITEURL وFORCE_SSL_ADMIN، وأضف إلى خدمة wordpress السطر ports: ["127.0.0.1:8080:80"]، ثم افتح نفق SSH Tunnel بالأمر ssh -L 8080:127.0.0.1:8080 [email protected] وافتح http://localhost:8080. ولكن الأفضل أن تكمل معالج التثبيت Installation Wizard على النطاق النهائي إن أمكن، والسبب أن WordPress يحفظ العنوان الذي ثبت عليه في إعداداته، فيبقى localhost في القاعدة حتى تستبدله.

بعد ذلك أنشئ شبكة proxy إذا لم تكن موجودة، وشغل الخدمات:

docker network create proxy
docker compose up -d
docker compose ps

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

NAME           IMAGE                           STATUS
wordpress      wordpress:7.1.2-php8.4-apache   Up 50 seconds (healthy)
wordpress-db   mariadb:11.8.9                  Up About a minute (healthy)

ربط النطاق وتشغيل HTTPS خلف الـ Reverse Proxy

في Nginx Proxy Manager أنشئ مضيفاً من نوع Proxy Host للنطاق blog.example.com يوجه الطلبات إلى http://wordpress:80، واطلب له شهادة Let's Encrypt Certificate، وفعل Block Common Exploits وForce SSL وHTTP/2.

والآن لاحظ ما يحدث للطلب: الـ Reverse Proxy ينهي اتصال TLS، ثم يمرر الطلب إلى WordPress عبر HTTP عادي، وبالتالي يرى WordPress طلباً غير مشفر، ولو صدق ذلك لحول الزائر إلى HTTPS، فيعيده الـ Reverse Proxy إليه عبر HTTP مرة أخرى، وهكذا تبدأ حلقة التحويل. والحل أن يعرف WordPress أن الزائر يستخدم HTTPS من الترويسة Header X-Forwarded-Proto: https التي يضيفها الـ Reverse Proxy، وملف wp-config.php في الـ Image الرسمية يعالج هذه الحالة مسبقاً، فيضبط $_SERVER['HTTPS'] = 'on' متى وجد الترويسة. ويمكنك أن تتحقق من ذلك بعد إكمال معالج التثبيت في القسم التالي، بأن تطلب صفحة الدخول من داخل الـ Container مرة دون الترويسة ومرة معها:

docker compose exec wordpress curl -sI -H 'Host: blog.example.com' http://localhost/wp-login.php | grep -iE '^(HTTP|Location)'
docker compose exec wordpress curl -sI -H 'Host: blog.example.com' -H 'X-Forwarded-Proto: https' http://localhost/wp-login.php | grep -iE '^(HTTP|Location)'

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

HTTP/1.1 302 Found
Location: https://blog.example.com/wp-login.php
HTTP/1.1 200 OK

دون الترويسة يحول WordPress الطلب إلى https:// بسبب FORCE_SSL_ADMIN، وهذا بالضبط ما يحدث في كل دورة من حلقة التحويل، ومع الترويسة يعرض الصفحة مباشرة بالرمز 200، وهذا ما يحدث للزائر الحقيقي خلف الـ Reverse Proxy.

⚠️
هذا الإعداد يصدق الترويسة أياً كان مصدرها، ولا يكون آمناً إلا لأن الـ Container الخاص بـ WordPress غير منشور والـ Reverse Proxy هو الطريق الوحيد إليه، لذلك لا تنشر المنفذ 80 الخاص بالـ Container على عنوان عام.

أما عنوان الزائر الحقيقي Client IP فيصل في X-Forwarded-For وX-Real-IP، ولكن WordPress لا يقرؤه تلقائياً، لذلك سوف تجد التعليقات وسجلات الدخول تحمل عنوان الـ Reverse Proxy، والحل أن تحدد الترويسة الموثوقة في إعدادات إضافة الأمان أو التخزين المؤقت Cache التي تستخدمها، وتجد شرح هذه الترويسات ومتى تثق بها في دليل تطبيقك خلف الـ Reverse Proxy.

إكمال معالج التثبيت

افتح https://blog.example.com، وسوف تجد أن الشاشة الأولى لاختيار لغة الموقع، فإذا اخترت العربية فسوف يحمل WordPress ملفات الترجمة، ويعمل الموقع ولوحة التحكم من اليمين إلى اليسار RTL:

شاشة اختيار لغة التثبيت في WordPress مع تحديد العربية
اختيار لغة الموقع

بعد ذلك يطلب منك معلومات الموقع وحساب المدير، ولا تستخدم اسم المستخدم Username admin، والسبب أنه أول اسم تجربه الهجمات الآلية Bots على أي موقع WordPress، واختر كلمة مرور طويلة يولدها مدير كلمات المرور Password Manager، وبريداً حقيقياً تصلك عليه رسائل استعادة كلمة المرور:

نموذج معلومات الموقع وحساب المدير في معالج تثبيت WordPress بالعربية
اسم الموقع وحساب المدير (أخفينا كلمة المرور)

اضغط «تنصيب ووردبريس» ثم سجل الدخول، وسوف تظهر لك لوحة التحكم:

لوحة تحكم WordPress 7.1.2 بالعربية
لوحة التحكم بعد التثبيت

إعدادات ما بعد التثبيت

تأتي الروابط افتراضياً بصيغة /?p=123، وهي صيغة لا تقول للزائر ولا لمحركات البحث شيئاً عن المقالة، ولتغييرها افتح الإعدادات ثم روابط دائمة، واختر «عنوان المقالة» (/%postname%/) واحفظ. والـ Image الرسمية بنسخة Apache تأتي مع mod_rewrite مفعلاً، وتكتب قواعد إعادة الكتابة Rewrite Rules القياسية في .htaccess عند التشغيل الأول، وحفظ الصفحة يحدث هذه القواعد، لذلك تعمل الروابط الجديدة مباشرة:

صفحة إعدادات الرابط الدائم في WordPress مع تحديد عنوان المقالة
بنية الروابط: عنوان المقالة

التحقق من حدود الرفع

افتح وسائط ثم إضافة ملف وسائط، وسوف ترى الحد الفعلي للرفع، ويجب أن يطابق ما كتبته في uploads.ini:

صفحة رفع الوسائط في WordPress تعرض الحد الأقصى 256 ميجابايت
الحد الأقصى للرفع: 256 ميجابايت كما ضبطناه

ولاحظ أن هذا الحد هو حد PHP وحده، فإذا رفعته فوق 100 ميجابايت تقريباً فراجع أيضاً حد الـ Reverse Proxy وحد أي Proxy آخر أمامه مثل Cloudflare، والسبب أن الملف يمر بكل هذه الطبقات، والحد الأصغر بينها هو الذي يقرر.

صفحة صحة الموقع Site Health

صفحة أدوات ثم صحة الموقع تفحص الإعداد كله، وإذا فتحتها في تجربة محلية قبل ضبط النطاق فسوف ترى ملاحظتين حرجتين عن REST API وloopback، والسبب أن الـ Container يحاول الاتصال بعنوان الموقع المحلي من داخله فلا يجده، وسوف ترى أيضاً ملاحظة «موقعك لا يستخدم HTTPS». أما على السيرفر الحقيقي حيث يشير النطاق إلى الـ Reverse Proxy فتختفي هذه الملاحظات، وإذا بقيت فراجع قسم المشكلات في آخر الدليل.

صفحة صحة الموقع في WordPress مع مشكلتين حرجتين وخمسة تحسينات موصى بها
صحة الموقع قبل ضبط النطاق، وينبغي مراجعتها بعد النقل إليه

بعد ذلك أغلق بقية الثغرات، فاحذف الإضافات والقوالب Themes التي لا تستخدمها (hello وakismet إن لم تحتج إليهما)، والسبب أن الإضافة المعطلة تبقى ملفاتها على السيرفر وثغراتها معها، وفعل التحديث التلقائي Auto Updates للإضافات الموثوقة، وأضف إضافة للتحقق بخطوتين Two-Factor Authentication لحسابات المدراء، وإضافة SMTP ترسل البريد عبر خدمة بريد حقيقية، لأن الـ Container لا يرسل البريد بنفسه.

واجهة موقع WordPress بالقالب الافتراضي تعرض مقالتين
الموقع بالقالب الافتراضي، ومعه مقالة مضافة عبر wp-cli

الإدارة من الطرفية Terminal بـ wp-cli

الأداة wp-cli تدير بها موقع WordPress دون متصفح، فتحدث النواة والإضافات وتدير المستخدمين وتبحث وتستبدل Search and Replace في القاعدة، ونشغلها عبر خدمة wpcli مؤقتة تشارك مجلد الموقع وشبكة القاعدة، ويحذف Compose الـ Container بعد انتهاء الأمر بسبب الخيار --rm:

cd /opt/wordpress
docker compose run --rm wpcli wp core version
docker compose run --rm wpcli wp --info

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

7.1.2
PHP version:	8.4.26
WP-CLI version:	2.12.0

وفيما يلي أوامر من الاستخدام اليومي:

docker compose run --rm wpcli wp plugin list
docker compose run --rm wpcli wp plugin delete hello
docker compose run --rm wpcli wp core check-update
docker compose run --rm wpcli wp user list --fields=user_login,roles
docker compose run --rm wpcli wp post create --post_title='مرحبا من wp-cli' --post_status=publish

انتبه عند إعادة توليد قواعد الروابط من wp-cli: الـ Container الخاص بـ wp-cli لا يعرف أن Apache فيه mod_rewrite، لذلك إذا نفذت wp rewrite flush --hard فسوف يظهر لك التحذير Regenerating a .htaccess file requires special configuration ولن يكتب شيئاً في .htaccess. وفي الحالة العادية لا يضرك هذا لأن القواعد مكتوبة من التشغيل الأول، ولكن إذا حذف الملف أو أفرغته إضافة أو تعديل يدوي فسوف تعيد كل الروابط 404 عدا الصفحة الرئيسية، والحل أن تخبر wp-cli بوجود mod_rewrite في ملف wp-cli.yml داخل مجلد الموقع، ثم تعيد توليد القواعد:

docker compose run --rm wpcli sh -c 'printf "apache_modules:\n  - mod_rewrite\n" > /var/www/html/wp-cli.yml && wp rewrite flush --hard'

وبعدها تفتح المقالات بالرمز 200 من جديد.

كيف تتأكد أن كل شيء يعمل

  • الأمر docker compose ps يعرض كلا الـ Containers بحالة healthy.
  • الأمر curl -sI https://blog.example.com يعيد الرمز 200، والأمر curl -sI http://blog.example.com يعيد الرمز 301 مع تحويل إلى https://.
  • مصدر الصفحة خال من روابط http://blog.example.com، أي لا يوجد محتوى مختلط Mixed Content يجعل المتصفح يحذر الزائر.
  • رابط مقالة بصيغة /اسم-المقالة/ يفتح، وصفحة الرفع تعرض الحد الذي ضبطته.
  • الأمر docker compose run --rm wpcli wp core check-update يعيد الرسالة Success: WordPress is at the latest version.
  • تثبيت إضافة من لوحة التحكم يكتمل دون أن يطلب منك بيانات FTP.

النسخ الاحتياطي Backup والاستعادة Restore

موقع WordPress يتكون من جزأين: قاعدة البيانات وفيها المقالات والإعدادات والمستخدمون، وwp-content وفيه الوسائط والإضافات والقوالب. أما ملفات النواة Core فلا تحتاج إلى نسخها، والسبب أن الـ Image تعيدها وتولد الملف wp-config.php من متغيرات البيئة Environment Variables، ولكن لا تنس أن تنسخ أيضاً compose.yaml و.env وuploads.ini، فبدون .env لن تعرف كلمة مرور القاعدة التي استعدتها.

ولتفريغ Dump القاعدة دون إيقاف الموقع نستخدم الأمر التالي، والخيار --single-transaction يعطيك Snapshot متسقة لجداول InnoDB حتى لو كتب أحد المحررين مقالة أثناء التفريغ:

cd /opt/wordpress
mkdir -p backups
docker compose exec -T db sh -c 'mariadb-dump --single-transaction -uwordpress -p"$MARIADB_PASSWORD" wordpress' | gzip > backups/db-$(date +%F).sql.gz

ولأرشفة wp-content نستخدم Container مؤقتاً يقرأ الـ Volume للقراءة فقط:

docker run --rm -v wordpress_html:/data:ro -v "$PWD/backups":/backup alpine:3.24 tar czf /backup/wp-content-$(date +%F).tar.gz -C /data wp-content

جدول الأمرين ليعملا يومياً عبر cron، وانقل مجلد backups إلى خارج السيرفر، واحذف منه النسخ القديمة. وللاستعادة على سيرفر جديد انسخ ملفات الإعداد، وشغل docker compose up -d لتحصل على قاعدة فارغة، ثم نفذ الأوامر التالية:

gunzip -c backups/db-2026-09-26.sql.gz | docker compose exec -T db sh -c 'mariadb -uwordpress -p"$MARIADB_PASSWORD" wordpress'
docker run --rm -v wordpress_html:/data -v "$PWD/backups":/backup alpine:3.24 sh -c 'rm -rf /data/wp-content && tar xzf /backup/wp-content-2026-09-26.tar.gz -C /data && chown -R 33:33 /data/wp-content'
docker compose restart wordpress

بعد الاستعادة تأكد أن عدد السجلات Rows في wp_posts يطابق عددها في القاعدة الأصلية، وأن صور المقالات تظهر، وجرب الاستعادة على سيرفر اختبار كل بضعة أشهر، والسبب أن النسخة الاحتياطية التي لم تجرب استعادتها لا يمكنك الاعتماد عليها يوم تحتاجها. وإذا تغير النطاق فاستبدله في القاعدة بالأمر docker compose run --rm wpcli wp search-replace 'https://old.example.com' 'https://blog.example.com' --skip-columns=guid، بعد أن تجربه مع --dry-run.

تحديث WordPress إلى إصدار أحدث Upgrade

هناك مشكلة رئيسية يقع فيها كثير ممن يشغلون WordPress في Docker، وهي أن نواة WordPress داخل Docker Image لا تتحدث بالطريقة التي يتوقعها، والسبب أن الـ Image تنسخ ملفات النواة إلى الـ Volume wordpress_html عند التشغيل الأول فقط إذا لم تجدها فيه، وبعد ذلك فتغيير وسم الـ Image يحدث إصدار PHP وApache، ولكنه لا يغير ملفات WordPress الموجودة في الـ Volume.

لذلك تحدث النواة من داخل WordPress نفسه، من لوحة التحكم أو بـ wp-cli أو بالتحديث التلقائي، ويجدر الانتباه هنا إلى أن التحديث التلقائي للنواة يكون مفعلاً في التثبيت الجديد للإصدارات الأمنية والرئيسية معاً، وهو يعمل في إعدادنا لأن الملفات مملوكة للمستخدم www-data وFS_METHOD مضبوط على direct، فلا تعطله ظناً منك أن وسم الـ Image هو الذي يحدد الإصدار.

📌
في 20 يناير 2017 أبلغت شركة Sucuri فريق WordPress بثغرة في REST API في الإصدارين 4.7 و4.7.1 تسمح لأي زائر دون تسجيل دخول بتعديل محتوى المقالات، فأصدر الفريق الإصدار 4.7.2 في 26 يناير وأخر الإعلان عن الثغرة أسبوعاً كاملاً، وكما شرح الفريق في إعلانه فقد وصل الإصلاح عبر نظام التحديث التلقائي إلى ملايين المواقع خلال ساعات دون أن يفعل أصحابها شيئاً. وبعد الإعلان بأيام رصدت Sucuri أربع حملات تشويه Defacement تمسح الإنترنت بحثاً عن المواقع التي لم تتحدث، وكان في نتائج Google لحملة واحدة منها أكثر من 66 ألف صفحة مخترقة خلال 48 ساعة، والدرس أن المواقع التي وصلها التحديث التلقائي نجت، وأن الموقع الذي عطل صاحبه التحديث بقي مكشوفاً لهذه الحملات.

وإذا أردت أن تحدث يدوياً فالخطوات كما يلي:

  1. خذ نسخة احتياطية من القاعدة وwp-content كما في القسم السابق.
  2. حدث النواة والإضافات والقوالب والترجمات بالأوامر التالية:
docker compose run --rm wpcli wp core update
docker compose run --rm wpcli wp core update-db
docker compose run --rm wpcli wp plugin update --all
docker compose run --rm wpcli wp theme update --all
docker compose run --rm wpcli wp language core update
  1. ولتحديث PHP أو Apache أو MariaDB غير الوسوم في compose.yaml، مثل وسم WordPress الجديد بأرقامه الكاملة وإصدار MariaDB 11.8 التالي، ثم نفذ:
docker compose pull
docker compose up -d
docker compose logs --tail 30 db

ابق على فرع MariaDB 11.8 LTS للتحديثات الإصلاحية Patch Releases، ولا تنتقل بين الإصدارات الرئيسية Major Versions، مثل الانتقال من 11.8 إلى 12.3، دون نسخة احتياطية وتجربة على سيرفر اختبار، وقبل أن ترفع إصدار PHP تأكد أن إضافاتك تدعمه، والسبب أن الإضافة القديمة التي لا تدعم الإصدار الجديد قد توقف الموقع كله بخطأ واحد.

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

رسالة Error establishing a database connection

السبب غالباً واحد من اثنين: الأول أن القاعدة لم تبدأ بعد، والثاني أن كلمة المرور في .env تغيرت بعد إنشاء القاعدة، والـ Image لا تقرأ متغيرات MARIADB_* إلا عند التهيئة الأولى Initialization، فتغييرها لاحقاً لا يغير كلمة المرور داخل القاعدة. راجع السجلات Logs بالأمر docker compose logs db، وإذا كنت غيرت كلمة المرور فأعدها إلى قيمتها السابقة، أو غيرها داخل القاعدة بالأمر ALTER USER.

الملف تجاوز upload_max_filesize

تأكد أولاً أن uploads.ini مركب داخل الـ Container بالأمر docker compose exec wordpress php -i | grep upload_max_filesize، فإذا كانت القيمة صحيحة وفشل الرفع بالرمز 413 فالحد مفروض في الـ Reverse Proxy أو في Cloudflare وليس في PHP.

WordPress يطلب بيانات FTP عند التحديث

السبب أن FS_METHOD غير مضبوط، أو أن الملفات ليست مملوكة للمستخدم www-data، وهذا يحدث عادة بعد نسخ الملفات بمستخدم آخر، ولإصلاح الملكية Ownership نفذ الأمر docker compose exec wordpress chown -R www-data:www-data /var/www/html/wp-content.

ERR_TOO_MANY_REDIRECTS بعد تفعيل HTTPS

WordPress لا يعرف أن الطلب وصل عبر HTTPS فيحوله، ثم يعيده الـ Reverse Proxy إليه، وتتكرر الحلقة، لذلك تأكد أن الـ Reverse Proxy يرسل X-Forwarded-Proto، وNginx Proxy Manager يفعل ذلك افتراضياً. وإذا كان Cloudflare أمام الـ Reverse Proxy فاجعل وضع SSL فيه Full (strict)، وإذا تعذر عليك دخول لوحة التحكم تماماً فعدل WP_HOME وWP_SITEURL في compose.yaml ثم أعد إنشاء الـ Container.

الروابط كلها تعيد 404 عدا الصفحة الرئيسية

قواعد .htaccess محذوفة أو فارغة، وهذا يحدث إذا حذف الملف أو أفرغته إضافة أو تعديل يدوي، والحل أن تحفظ صفحة الروابط الدائمة من لوحة التحكم مرة واحدة، أو تستخدم الأمر wp rewrite flush --hard مع wp-cli.yml كما في قسم wp-cli.

صفحة صحة الموقع تعرض خطأ في loopback أو REST API

الـ Container لا يصل إلى رابط الموقع العام من داخله، فتفشل المهام المجدولة Cron Jobs وبعض وظائف المحرر، وتتحقق من ذلك بالأمر docker compose exec wordpress curl -sI https://blog.example.com، فإذا فشل فالسبب غالباً DNS داخلي أو Router لا يدعم NAT loopback، والحل أن تضيف إلى خدمة wordpress السطر extra_hosts: ["blog.example.com:host-gateway"] حتى يصل الـ Container إلى الـ Reverse Proxy على السيرفر نفسه.

المقالات المجدولة Scheduled Posts لا تنشر في موعدها

WP-Cron لا يعمل إلا حين يزور أحد الموقع، لذلك في المواقع قليلة الزيارات أضف define('DISABLE_WP_CRON', true); إلى WORDPRESS_CONFIG_EXTRA، ثم شغل المهام من cron السيرفر كل خمس دقائق بالأمر cd /opt/wordpress && docker compose run --rm wpcli wp cron event run --due-now.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • الاستضافة المشتركة والتثبيت بنقرة واحدة لا يعطيانك تحكماً في PHP والحدود والنسخ الاحتياطي، والاستضافة المدارة مريحة ولكنها مكلفة ومقيدة، وملف Compose خاص بك على سيرفرك يجمع التحكم الكامل مع إعداد تعرف كل سطر فيه.
  • ولد كلمات المرور في .env، وحدد وسوم الـ Images بأرقامها الكاملة، واجعل post_max_size أكبر قليلاً من upload_max_filesize.
  • لا تنشر منفذ WordPress، فالـ Image تصدق الترويسة X-Forwarded-Proto، وهذا آمن فقط حين يكون الـ Reverse Proxy هو الطريق الوحيد إلى الـ Container.
  • شغل wp-cli بالمستخدم 33 ومعه HOME: /tmp، وأخبره بوجود mod_rewrite في wp-cli.yml قبل أن تعيد توليد قواعد .htaccess.
  • انسخ القاعدة وwp-content ومعهما .env، وجرب الاستعادة على سيرفر اختبار، واترك التحديث التلقائي للنواة يعمل، لأن تغيير وسم الـ Image لا يحدث ملفات WordPress.

سجل التحديثات Changelog

  • سبتمبر 2026: كتابة الدليل واختباره على WordPress 7.1.2 مع MariaDB 11.8.9.
  • أكتوبر 2026: مراجعة الدليل وإعادة اختباره، ورفع post_max_size فوق حد الملف، وإضافة HOME: /tmp إلى خدمة wp-cli، وتصحيح طريقة التحقق من HTTPS خلف الـ Reverse Proxy وشرح سلوك wp-cli مع .htaccess، وتوضيح موقع MariaDB 11.8 بعد صدور 12.3 LTS والتحديث التلقائي للنواة.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.