استضافة ووردبريس ذاتيًا مع Docker: أباتشي أو PHP-FPM، مع ذاكرة كائنات على Redis
كل دروس ووردبريس على Docker تتوقف عند "إنه يعمل". هذا الدليل يمضي أبعد: إصدارات مثبّتة، وذاكرة كائنات حقيقية مع امتداد PHP المُصرَّف، ووكيل عكسي يُنهي TLS فعلًا، والسلوكان أو الثلاثة في الصورة الرسمية التي ستلدغك في اليوم الثلاثين لا في اليوم الأول.
الإصدارات أدناه كانت هي الحالية عند تحققي منها في 2 أغسطس 2026. ثبّتها. ولا تنشر :latest في الإنتاج.
| المكوّن | الوسم | ملاحظات |
|---|---|---|
| WordPress | wordpress:7.0.2-php8.5-apache | أو 7.0.2-php8.5-fpm-alpine |
| PHP | 8.5 | مضمّن في صورة ووردبريس |
| MariaDB | mariadb:12.3 | يشير lts حاليًا إلى 12.3.2 |
| Redis | redis:8.10-alpine | الخادم فقط، دون حفظ دائم |
| nginx | nginx:1.30.4-alpine | فقط إن شغّلته كحاوية |
| Redis Object Cache | 2.8.0 | إضافة، تأتي معها Predis 2.4.0 |
| phpredis | 6.3.0 | اختياري، انظر الخطوة 3 |
| WP-CLI | wordpress:cli-php8.5 | حاوية مرافقة، تُشغَّل عند الحاجة |
يصدر ووردبريس 7.1 في 19 أغسطس 2026، فتوقّع أن يتغير الجزء 7.0.2 من ذلك الوسم قريبًا.
المنظومة
ثلاث حاويات تقوم بالعمل. لا شيء يمكن الوصول إليه من الخارج سوى المنفذ الوحيد الذي تنشره، وأنت تنشره على الاسترجاع المحلي فقط.
[ nginx, terminating TLS ]
|
v
127.0.0.1:8080
|
===========|=================== docker
v
[ wp_app ] wordpress + php
|
| service names on wp_default
+--------+--------+
| |
[ wp_db ] [ wp_redis ]
:3306 :6379قاعدة البيانات وRedis لا ينشران منفذًا أبدًا. يمكن الوصول إليهما من الحاويات الأخرى باسم الخدمة، db وredis، ومن لا مكان غير ذلك.
قراران
كيف تعمل PHP. أباتشي مع mod_php في حاوية واحدة، أو PHP-FPM. أباتشي يعني صورة واحدة وعملية واحدة وبلا ملف إعدادات إضافي. أما FPM فيمنحك خادم ويب منفصلًا يقدّم الملفات الساكنة دون المرور بـ PHP، ويكلفك إعداد nginx صار الآن على عاتقك.
أين يعمل nginx. على المضيف، أو كحاوية رابعة.
edge = nginx on the host edge = nginx in compose
A) apache image publish 127.0.0.1:8080 proxy_pass wordpress:80
proxy_pass to it 4 containers
3 containers <-- start here
B) fpm image publish 127.0.0.1:9000 fastcgi_pass wordpress:9000
fastcgi_pass to it 4 containers
3 containersابدأ بالمسار A مع nginx على المضيف. فهو أقل الخيارات أجزاءً متحركة، وcertbot على المضيف يتولى التجديدات دون أن تفكر فيها. انتقل إلى B إن أردت تخزينًا مؤقتًا وتحديدًا للمعدل لكل موقع مسار، أو إن كنت تقدّم من الملفات الساكنة ما يجعل إبقاءها بعيدًا عن PHP أمرًا مهمًا.
الخطوات من 1 إلى 5 متطابقة في كل التركيبات.
المتطلبات المسبقة
Docker Engine 27 أو أحدث مع إضافة Compose v2. هذه هي القائمة كاملة.
ضع المشروع في مكان يستطيع nginx المرور عبره. إن كان nginx على المضيف سيقرأ ./data/wordpress مباشرة (المسار B)، فلن يعمل /root/wp مهما غيّرت الأذونات، لأن /root أذونـاته 700. استخدم /srv/wp أو /opt/wp.
sudo mkdir -p /srv/wp && cd /srv/wpالخطوة 1: هيكل المجلدات
/srv/wp/
├─ .env # secrets and sizing
├─ compose.yaml
├─ Dockerfile # adds phpredis
├─ config/
│ ├─ php.ini # PHP + opcache overrides
│ └─ nginx.conf # only if nginx runs as a container
├─ backup/
└─ data/
├─ db/ # MariaDB datadir
└─ wordpress/ # core + wp-contentmkdir -p config backup data/db data/wordpressلا مجلد بيانات لـ Redis. مزيد من التفصيل في الخطوة 4.
الخطوة 2: ملف .env
# Database
MARIADB_ROOT_PASSWORD=change_me_root
MARIADB_DATABASE=wordpress
MARIADB_USER=wordpress
MARIADB_PASSWORD=change_me_user
WORDPRESS_TABLE_PREFIX=wp_
# Where the stack listens on the host (loopback only)
HTTP_BIND=127.0.0.1:8080
# WordPress memory
WP_MEMORY_LIMIT=256M
WP_MAX_MEMORY_LIMIT=512M
# Redis
REDIS_MAXMEMORY=256mb
WP_REDIS_PREFIX=wpولّد كلمات المرور بدل كتابتها:
openssl rand -base64 24ثم chmod 600 .env وأضفه إلى .gitignore.
يصبح WP_REDIS_PREFIX مهمًا لحظة تشغيلك موقع ووردبريس ثانيًا. موقعان يتشاركان Redis واحدًا دون بادئة سيقرأ كلٌّ منهما مفاتيح ذاكرة الآخر، وتبدو الأعراض كأن المكان مسكون.
الخطوة 3: اختيار عميل Redis
يمكنك تخطي هذه الخطوة. وهي موجودة لأن أمام PHP طريقتين للتحدث إلى Redis، وكثير من الأدلة تقدّم الأصعب منهما على أنها إلزامية.
تأتي صورة ووردبريس الرسمية مع bcmath وexif وgd وintl وmysqli وzip وimagick. ولا تأتي مع امتداد Redis. وهذا لا يهم، لأن إضافة Redis Object Cache تحمل معها Predis، وهو عميل Redis مكتوب بـ PHP خالصة، وتستخدمه تلقائيًا عند غياب الامتداد.
phpredis (PECL) Predis (bundled)
client language C extension PHP
in stock image no, must build yes, ships with plugin
custom Dockerfile required not needed
rebuild on every yes no
upstream update
plugin support yes yes, the default fallbackاستخدم الصورة كما هي. شغّل الذاكرة المؤقتة أولًا، ثم افتح تبويب Metrics في الإضافة وانظر إلى الأجزاء من الألف من الثانية التي تُبلغ عنها لكل طلب. إن أزعجك ذلك الرقم، فعُد وصرِّف الامتداد. في موقع تعريفي بسيط لن تلمس فرقًا. أما في متجر WooCommerce بأربعين إضافة تُجري مئات النداءات لـ wp_cache_get في الصفحة الواحدة، فستلمسه.
تكلفة الصورة المخصصة ليست ملف Dockerfile، بل أنك صرت تعيد البناء كلما أصدر المنبع إصلاحًا أمنيًا لووردبريس أو لـ PHP. اعرف ما الذي تشتريه.
إن أردتها، أضف ملف Dockerfile بجوار compose.yaml.
أباتشي (أساس دبيان، وPHPIZE_DEPS مثبّتة سلفًا):
FROM wordpress:7.0.2-php8.5-apache
RUN set -eux; \
pecl install redis-6.3.0; \
docker-php-ext-enable redis; \
rm -rf /tmp/pearFPM على Alpine (أدوات البناء غير مثبّتة مسبقًا، لذا تُضاف ثم تُزال):
FROM wordpress:7.0.2-php8.5-fpm-alpine
RUN set -eux; \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS; \
pecl install redis-6.3.0; \
docker-php-ext-enable redis; \
apk del .build-deps; \
rm -rf /tmp/pearثم استبدل image: بـ build: في الخطوة التالية. تلتقط الإضافة الامتداد من تلقاء نفسها؛ ويمكنك أيضًا تثبيت الاختيار عبر define('WP_REDIS_CLIENT', 'phpredis'); أو 'predis'.
الخطوة 4: config/php.ini
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
max_input_vars = 3000
; the base image sets 4000 files and revalidate_freq=2,
; both too tight for a plugin-heavy production site
opcache.memory_consumption = 192
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60من الأنماط الشائعة تمرير هذه القيم كمتغيرات بيئة وإعادة كتابتها في ملف ini عبر تجاوز command الخاص بالحاوية. تجاوز هذا النمط. تجاوز command يعني أنك صرت تصون سطر صدفة واحدًا يجب أن ينتهي بـ docker-entrypoint.sh apache2-foreground، وخطأ مطبعي واحد هناك يكلفك نقطة الدخول التي تفك ضغط ووردبريس وتكتب wp-config.php. أما الملف المُركّب فهو سطر واحد من YAML ويبقى عبر كل ترقية للصورة.
البادئة zz- في وجهة التركيب تجعل PHP تحمّله أخيرًا، فيتغلب على ملف opcache-recommended.ini الخاص بالصورة.
الخطوة 5: compose.yaml
هذا هو المسار A: أباتشي، منشور على الاسترجاع المحلي ليتولى nginx على المضيف الوساطة أمامه.
name: wp
services:
db:
image: mariadb:12.3
container_name: wp_db
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?missing in .env}
MARIADB_DATABASE: ${MARIADB_DATABASE:-wordpress}
MARIADB_USER: ${MARIADB_USER:-wordpress}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:?missing in .env}
MARIADB_AUTO_UPGRADE: "1"
volumes:
- ./data/db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
redis:
image: redis:8.10-alpine
container_name: wp_redis
restart: unless-stopped
command: >
redis-server
--save ""
--appendonly no
--maxmemory ${REDIS_MAXMEMORY:-256mb}
--maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
wordpress:
image: wordpress:7.0.2-php8.5-apache
# only if you compiled phpredis in step 3:
# build:
# context: .
# dockerfile: Dockerfile
# image: wp-wordpress:local
container_name: wp_app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${HTTP_BIND:-127.0.0.1:8080}:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-wordpress}
WORDPRESS_DB_USER: ${MARIADB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:?missing in .env}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-wp_}
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PREFIX', '${WP_REDIS_PREFIX:-wp}');
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
define('WP_CACHE', true);
define('WP_MEMORY_LIMIT', '${WP_MEMORY_LIMIT:-256M}');
define('WP_MAX_MEMORY_LIMIT', '${WP_MAX_MEMORY_LIMIT:-512M}');
define('DISALLOW_FILE_EDIT', true);
define('FS_METHOD', 'direct');
volumes:
- ./data/wordpress:/var/www/html
- ./config/php.ini:/usr/local/etc/php/conf.d/zz-wordpress.ini:ro
wpcli:
image: wordpress:cli-php8.5
profiles: [tools]
user: "33:33"
depends_on:
db:
condition: service_healthy
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-wordpress}
WORDPRESS_DB_USER: ${MARIADB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:?missing in .env}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-wp_}
volumes:
- ./data/wordpress:/var/www/htmlلا كتلة networks:. يضع Compose كل خدمة على شبكة مشروع اسمها wp_default ويمنح كلًا منها اسم DNS مطابقًا لاسم خدمتها، ولهذا فإن WORDPRESS_DB_HOST هو db:3306. وتعريف الشبكات يدويًا لا يضيف شيئًا هنا.
خمسة أمور تستحق التنويه.
عنوان الربط ليس اختياريًا. كتابة "8080:80" تنشر على كل الواجهات، ويُدرج Docker قواعد iptables الخاصة به قبل قواعدك، فيخبرك ufw بكل اطمئنان أن المنفذ 8080 محجوب بينما الإنترنت كله يقرأ ووردبريس لديك عبر HTTP الصريح. والحل هو 127.0.0.1:8080:80.
${VAR:?message} يُجهض docker compose up مع رسالتك عند غياب متغير. وبدونه، يمنحك خطأ مطبعي في .env قاعدة بيانات بكلمة مرور فارغة.
لا وحدة تخزين لـ Redis، --save ""، --appendonly no. ذاكرة الكائنات بيانات مشتقة. حفظها دائمًا يشتري لك ذاكرة دافئة بعد إعادة التشغيل ويكلفك كتابات قرص قائمة على fork إلى الأبد. والذاكرة الباردة تمتلئ من جديد خلال ثوانٍ. وإن استخدمت Redis لاحقًا للجلسات أو لطابور مهام، فسيتغير هذا الحساب.
--maxmemory-policy allkeys-lru هو الخيار المهم. إذ يعتمد Redis افتراضيًا noeviction، ما يعني أنه بمجرد امتلاء الذاكرة تبدأ عمليات الكتابة بالفشل ويبدأ ووردبريس بإطلاق أخطاء ذاكرة بدل أن يُزيح المفاتيح الأبرد بهدوء.
تعمل فحوص السلامة كل 10 ثوانٍ، لا كل 120 ثانية. ينتظر depends_on: service_healthy فحصًا ناجحًا، فتتحول الفترة البطيئة مباشرة إلى بدء تشغيل بطيء. ودقيقتان تبدو فيهما المنظومة معطلة ليستا فضيلة.
إن نقلت معك كتلة volumes: في المستوى الأعلى تُعرّف db_data وأخواتها، فاحذفها. تلك الوحدات المسماة تبقى بلا استخدام حين تُركّب ./data/db كربط مباشر.
الخطوة 6: nginx على المضيف
ثبّت nginx وcertbot، ثم ضع ملف مضيف افتراضي:
sudo apt install nginx certbot python3-certbot-nginx# /etc/nginx/sites-available/example.com
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
client_max_body_size 64m; # keep in sync with post_max_size
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s;
}
}sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com -d www.example.comX-Forwarded-Proto هو الترويسة التي يُعوَّل عليها. فعلى أساسها يقرر ووردبريس إصدار روابط بـ http:// أو https://، وملف wp-config في الصورة الرسمية يقرؤها أصلًا (انظر الخطوة 8). أسقط تلك الترويسة وستحصل على تحذيرات محتوى مختلط، أو حلقة إعادة توجيه.
اختلاف client_max_body_size عن post_max_size هو السبب الأشيع على الإطلاق وراء "رفع الفيديو بحجم 30 ميغابايت يتوقف عند 90%". فـ nginx يرفض جسم الطلب قبل أن تراه PHP أصلًا، ورفع حد PHP وحده لا يوصلك إلى شيء.
يعيد Certbot كتابة الملف ليضيف كتلة 443 وإعادة توجيه، ويثبّت مؤقت تجديد. تحقق منه بـ systemctl list-timers | grep certbot.
الخطوة 6، المسار B: PHP-FPM
تغييران في ملف compose. وجّه البناء إلى ملف Dockerfile الخاص بـ FPM، وانشر FastCGI بدل HTTP:
wordpress:
image: wordpress:7.0.2-php8.5-fpm-alpine
# or build: from the fpm-alpine Dockerfile in step 3
container_name: wp_app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "127.0.0.1:9000:9000" # FastCGI, loopback only, no exceptions
environment:
# unchanged from route A
...
volumes:
- ./data/wordpress:/var/www/html
- ./config/php.ini:/usr/local/etc/php/conf.d/zz-wordpress.ini:roالمنفذ 9000 يتحدث FastCGI لا HTTP. وFastCGI بلا مصادقة وبلا TLS، وأي شيء يصل إليه يستطيع تنفيذ PHP كيفما شاء. وإن وجدت نفسك يومًا تكتب 9000:9000 دون بادئة الاسترجاع المحلي، فتوقف.
الآن يقدّم nginx على المضيف الملفات الساكنة بنفسه ولا يمرّر إلى الحاوية سوى PHP:
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
root /srv/wp/data/wordpress; # the HOST path
index index.php;
client_max_body_size 64m;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
try_files $uri =404;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
# the container sees these files at /var/www/html, not at the host path
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/html;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
location ~* \.(js|css|png|jpe?g|gif|ico|svg|webp|avif|woff2?)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
try_files $uri =404;
}
# never execute PHP that arrived through the media uploader
location ^~ /wp-content/uploads/ {
location ~ \.php$ { deny all; }
}
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; allow all; }
location ~ /\. { deny all; }
location = /xmlrpc.php { deny all; } # drop this line if you use the WP mobile app
}هذان التجاوزان لـ fastcgi_param هما الحيلة كلها، وإغفالهما هو سبب التخلي عادةً عن هذا الإعداد. يحسب nginx قيمة SCRIPT_FILENAME من root الخاص به، وهو /srv/wp/data/wordpress. عندئذٍ يبحث PHP-FPM عن ذلك المسار داخل الحاوية، حيث لا وجود له، فيعيد "Primary script unknown" أو صفحة بيضاء فارغة بلا شيء مفيد في سجل nginx. نظاما الملفات هما الملفات نفسها في مسارين مختلفين، وأنت وحدك تعرف المقابلة بينهما.
ملاحظتان أصغر حول هذا المسار. try_files $uri =404 داخل كتلة PHP تغلق ثغرة تنفيذ الشيفرة عبر path-info التي ما زالت الإعدادات الجاهزة من عام 2013 تحملها. كما يعمل كشف HTTPS هنا بطريقة مختلفة: لا وكيل عكسي، وبالتالي لا X-Forwarded-Proto. فملف fastcgi_params في دبيان يمرّر HTTPS=on مباشرة بمجرد أن يُعد certbot كتلة TLS، ويلتقط ووردبريس ذلك من تلقاء نفسه.
الخطوة 6، بديل: nginx كحاوية
استخدم هذا إن لم يكن على المضيف nginx وفضّلت إبقاء كل شيء داخل Docker. أضف خدمة رابعة وتوقف عن نشر منفذ من wordpress:
nginx:
image: nginx:1.30.4-alpine
container_name: wp_nginx
restart: unless-stopped
depends_on: [wordpress]
ports:
- "80:80"
- "443:443"
volumes:
- ./data/wordpress:/var/www/html:ro # only needed for the fpm variant
- ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./config/certs:/etc/nginx/certs:ro # if you have certs alreadyمع صورة أباتشي، يكون config/nginx.conf هو إعداد الوساطة من الخطوة 6 مع proxy_pass http://wordpress:80; بدل 127.0.0.1:8080. ومع صورة FPM، يكون إعداد FastCGI مع fastcgi_pass wordpress:9000; وroot /var/www/html; وبلا إعادة ربط للمسارات، لأن الحاويتين داخل Docker تتفقان على المسار نفسه.
كن واضحًا مع نفسك بشأن TLS هنا. يكون nginx داخل حاوية منطقيًا حين تمتلك الشهادات على القرص أصلًا، أو حين يكون الموقع داخليًا ويعمل على HTTP صريح، أو حين يتولى شيء أمامك إنهاء TLS نيابة عنك. أما تشغيل تجديد Let's Encrypt تلقائيًا داخل هذه الحاوية فيعني حاوية certbot مرافقة، وجذر ويب مشتركًا، وخطاف إعادة تحميل. وإن لم يكن هذا مشروعًا يستهويك، فاستخدم nginx على المضيف، أو استبدل هذه الخدمة بـ Caddy أو Traefik، وكلاهما يتولى ACME بنفسه.
الخطوة 7: أول تشغيل
docker compose up -d
docker compose logs -f wordpressلا تشغّل docker compose build أولًا إلا إن اخترت الصورة المخصصة في الخطوة 3.
أنت تترقب سطرين:
WordPress not found in /var/www/html - copying now...
No 'wp-config.php' found in /var/www/html, but 'WORDPRESS_...' variables supplied; copying ...هكذا يتتابع الإقلاع تقريبًا:
t=0s db starting health: starting
t~20s db healthy healthcheck.sh --connect passes
t~20s redis healthy
t~21s wordpress starts unpack core -> generate wp-config.phpتحقق من استجابة المنظومة على الاسترجاع المحلي قبل إقحام nginx. فهذه أسرع طريقة لمعرفة أي النصفين معطل:
curl -I http://127.0.0.1:8080 # route A
# HTTP/1.1 302 Found
# Location: http://127.0.0.1:8080/wp-admin/install.phpثم افتح النطاق وأكمل التثبيت الذي يستغرق خمس دقائق.
الخطوة 8: ما تكتبه الصورة في wp-config.php
ثلاث حقائق يُستحسن معرفتها قبل أن تحتاج إليها.
تُولَّد الأملاح مرة واحدة. تأتي مفاتيح المصادقة والأملاح من /dev/urandom لحظة إنشاء الملف، وتُكتب فيه حرفيًا. ولا تُقرأ من البيئة عند كل إقلاع. ولإدارتها بنفسك، اضبط WORDPRESS_AUTH_KEY وWORDPRESS_SECURE_AUTH_KEY وWORDPRESS_LOGGED_IN_KEY وWORDPRESS_NONCE_KEY وWORDPRESS_AUTH_SALT وWORDPRESS_SECURE_AUTH_SALT وWORDPRESS_LOGGED_IN_SALT وWORDPRESS_NONCE_SALT قبل أول تشغيل. وتغييرها لاحقًا يُخرج الجميع من حساباتهم، وهو تحديدًا ما تريده بعد اختراق.
كل ما عداها يُقرأ مباشرة. يمر WORDPRESS_CONFIG_EXTRA ومتغيرات WORDPRESS_DB_* عبر مساعد اسمه getenv_docker() في كل طلب، وتُنفَّذ الكتلة الإضافية عبر eval(). لذا فإن تعديل WORDPRESS_CONFIG_EXTRA ثم تشغيل docker compose up -d يسري مفعوله فورًا. ولا تحتاج إلى حذف wp-config.php.
معالجة الوكيل العكسي موجودة سلفًا. لا حاجة لإضافتها:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
$_SERVER['HTTPS'] = 'on';
}وإن ظللت تحصل على إعادة توجيه HTTPS لا تنتهي، فالمشكلة أمامك: nginx لا يرسل الترويسة.
الخطوة 9: تشغيل ذاكرة الكائنات
ثلاثة أوامر، على الصورة كما هي:
docker compose run --rm wpcli plugin install redis-cache --activate
docker compose run --rm wpcli redis enable
docker compose run --rm wpcli redis statusيخبرك redis status بالعميل الذي وقع عليه الاختيار:
Status: Connected
Client: Predis (v2.4.0)
Drop-in: ValidClient: PhpRedis (v6.3.0) تعني أن الامتداد موجود وقيد الاستخدام. وأي من السطرين يعني ذاكرة عاملة. تأكد من الامتداد على حدة بـ docker compose exec wordpress php -m | grep -x redis، وهو لا يطبع شيئًا على الصورة كما هي، وذلك متوقع.
يضع redis enable ملف wp-content/object-cache.php في مكانه. وذلك الملف هو الآلية بأكملها. وبدونه لا يفعل WP_CACHE ولا ثوابت WP_REDIS_* أي شيء على الإطلاق. وتثبيت الإضافة وتفعيلها لا يكفيان وحدهما؛ إذ يجب أن يوجد الملف المُسقط، وهذا أيضًا سبب ظهور فشل الكتابة في wp-content على هيئة ذاكرة لا تفعل شيئًا في صمت.
معرّف المستخدم في خدمة wpcli ليس زينة. فصورة سطر الأوامر مبنية على Alpine، حيث معرّف www-data هو 82. وصورة ووردبريس مع أباتشي مبنية على دبيان، حيث معرّف www-data هو 33. شغّل WP-CLI بمعرّف خاطئ وسيصبح كل ملف يكتبه غير قابل للقراءة من PHP. طابقه مع صورتك الأساسية:
wordpress:...-apache Debian www-data = 33 -> user: "33:33"
wordpress:...-fpm Debian www-data = 33 -> user: "33:33"
wordpress:...-fpm-alpine Alpine www-data = 82 -> user: "82:82"تحقق من وصول المفاتيح إلى Redis، ثم حمّل الموقع بضع مرات وراقب الأرقام وهي ترتفع:
docker compose exec redis redis-cli info keyspace
# db0:keys=412,expires=7,avg_ttl=0
docker compose exec redis redis-cli info stats | grep keyspace
# keyspace_hits:18342
# keyspace_misses:611هكذا يبدو مسار الطلب بعد أن يعمل:
request
|
v
[ nginx ] -> [ apache | php-fpm ]
|
v
[ PHP: wp_cache_get ]
|
+-- HIT --> render from Redis, ~2 SQL queries
|
+-- MISS --> MariaDB --> wp_cache_set --> renderنسبة إصابة دون 90% تقريبًا بعد يوم من الزيارات تعني عادةً مشكلة إزاحة، لا مشكلة في ووردبريس. تحقق مما إذا كان used_memory قد التصق بـ maxmemory وارفع REDIS_MAXMEMORY.
ذاكرة الكائنات ليست ذاكرة صفحات. فهي تزيل استعلامات قاعدة البيانات المتكررة من كل طلب، بما في ذلك طلبات المستخدمين المسجَّلين وطلبات لوحة التحكم. لكنها لا تمنع ووردبريس من إقلاع PHP. أما تخزين الصفحات كاملة فمكانه nginx أو إضافة، وهو قرار منفصل.
الخطوة 10: النسخ الاحتياطي
لا تحزم بـ tar مجلد بيانات MariaDB وهو قيد التشغيل. فالكتابة جارية في ./data/db أثناء إنشاء الأرشيف، وقد تكون النسخة التي تستعيدها متسقة وقد لا تكون. صدّر قاعدة البيانات وأرشف الملفات على حدة.
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
source .env
stamp=$(date +%Y%m%d-%H%M)
docker compose exec -T db \
mariadb-dump --single-transaction --quick --no-tablespaces \
-u root -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" \
| gzip > "backup/db-$stamp.sql.gz"
tar -czf "backup/files-$stamp.tar.gz" -C data/wordpress wp-content
find backup -name '*.gz' -mtime +14 -delete--single-transaction يمنحك لقطة متسقة على InnoDB دون قفل الكاتبين. وفي MariaDB 11 وما بعدها صار اسم الملف التنفيذي mariadb-dump؛ وما زال mysqldump موجودًا كرابط رمزي، لكن ليس إلى الأبد.
الاستعادة:
gunzip -c backup/db-20260802-1130.sql.gz | \
docker compose exec -T db mariadb -u root -p"$MARIADB_ROOT_PASSWORD" wordpressوحده wp-content يستحق الأرشفة. فملفات النواة تأتي من الصورة، وwp-config.php يمكن إعادة إنتاجه من ملف compose لديك، باستثناء تلك الأملاح. خذ نسخة احتياطية من الأملاح مرة واحدة.
الخطوة 11: التحديث
وهي النقطة التي تفاجئ الناس. انظر إلى الشرط الحارس في نقطة الدخول:
if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then
echo >&2 "WordPress not found in $PWD - copying now..."تُنسخ ملفات النواة من الصورة فقط حين يكون المجلد فارغًا. وبمجرد أن يحتوي ./data/wordpress على تثبيت، فإن سحب wordpress:7.1-php8.5-apache يمنحك بناء PHP الأحدث وأباتشي الأحدث والامتدادات الأحدث، ويترك ملفات نواة ووردبريس لديك تمامًا حيث كانت.
لذا ينقسم التحديث إلى شقين:
Image tag -> PHP, Apache/FPM, extensions, base OS
docker compose pull && docker compose up -d
(add `docker compose build --pull` if you use the custom image)
WP core -> dashboard, or:
docker compose run --rm wpcli core update
docker compose run --rm wpcli core update-db
docker compose run --rm wpcli plugin update --allتحتاج الإصدارات الكبرى من MariaDB إلى MARIADB_AUTO_UPGRADE: "1"، وهو موجود سلفًا في ملف compose أعلاه. فهو يشغّل mariadb-upgrade عندما يكتشف مجلد بيانات أقدم. خذ نسخة تصدير أولًا على أي حال، وتنقّل بين الإصدارات الكبرى واحدًا تلو الآخر.
معالجة المشكلات
| العَرَض | السبب المرجَّح |
|---|---|
| "Error establishing a database connection" عند أول تشغيل | جرت تهيئة ./data/db بكلمة مرور مختلفة في تشغيل سابق. وصورة MariaDB لا تطبّق بيانات الاعتماد إلا على مجلد بيانات فارغ. امسحه أو أعد ضبط كلمة المرور يدويًا |
| 502 Bad Gateway من nginx على المضيف | لا شيء يستمع على 127.0.0.1:8080. تحقق من docker compose ps وss -ltnp | grep 8080 |
| "Primary script unknown"، المسار B | ما زال SCRIPT_FILENAME يشير إلى مسار المضيف. تجاوزه إلى /var/www/html$fastcgi_script_name |
| 403 من nginx على الملفات الساكنة، المسار B | لا يستطيع nginx المرور إلى ./data/wordpress. انقل المشروع خارج مجلد أب أذوناته 700 |
الموقع يُحمَّل لكن كل الروابط http:// | لا يضبط nginx الترويسة X-Forwarded-Proto |
| إعادة توجيه HTTPS لا تنتهي | السبب نفسه أعلاه |
| حد الرفع ما زال يظهر 2M | ملف ini غير مُركّب، أو مُركّب خارج /usr/local/etc/php/conf.d/. تحقق بـ docker compose exec wordpress php -i | grep upload_max |
| الرفع يتوقف في منتصفه | قيمة client_max_body_size في nginx أصغر من post_max_size في PHP |
| شاشة بيضاء بعد تشغيل ذاكرة الكائنات | ملف wp-content/object-cache.php قديم. احذفه ثم شغّل wpcli redis enable من جديد |
| الإضافة مفعّلة، لكن لا شيء في Redis | لم يُكتب الملف المُسقط أصلًا. وسيقول redis status إن الملف المُسقط غير صالح. تأكد من إمكانية الكتابة في wp-content |
redis status يقول "Not connected" | قيمة WP_REDIS_HOST خاطئة، أو الحاويات في مشاريع Compose مختلفة |
بنيتَ الصورة المخصصة لكن redis status ما زال يقول Predis | ما زالت الحاوية تعمل بالصورة القديمة. docker compose up -d --force-recreate wordpress |
| تثبيت الإضافات يطلب بيانات اعتماد FTP | FS_METHOD ليس مضبوطًا على direct، أو أن مستخدم PHP لا يستطيع الكتابة في wp-content |
| سُحبت صورة جديدة وإصدار ووردبريس لم يتغير | هذا متوقع. حدّث النواة عبر WP-CLI أو من لوحة التحكم |
| نسبة إصابة الذاكرة عالقة عند 60% تقريبًا | بلغ Redis حد maxmemory وصار يُزيح. ارفع REDIS_MAXMEMORY |
ما العمل بعد ذلك
ضع سكربت النسخ الاحتياطي من الخطوة 10 في مهمة cron، واختبر الاستعادة في مجلد مؤقت قبل أن تحتاج إليها. ثم قِس: ثبّت Query Monitor، وسجّل عدد الاستعلامات في أبطأ صفحاتك مع تعطيل ذاكرة الكائنات، ثم فعّلها وانظر مرة أخرى. ذلك الرقم هو الدليل الوحيد على أن أيًا من هذا قد نفع.
بعد ذلك، الثغرة التي يستحق سدّها هي أن تعرف بأن الخادم في ضيق قبل أن يخبرك زائر. وPrometheus وNode Exporter وGrafana يهيّئ ذلك على النوع نفسه من الخوادم المفردة، ولوحة Node Exporter وحدها ستخبرك إن كان حجم Redis مضبوطًا فعلًا.
الاستضافة الذاتية هي معظم ما أعمل عليه: أكثر من 30 منصة مفتوحة المصدر جرى نشرها وصيانتها لعملاء خلال العقد الماضي. وإن كنت تفضّل ألا تدير هذا بنفسك، تواصل معي.