ডকার দিয়ে ওয়ার্ডপ্রেস সেলফ-হোস্টিং: Apache নাকি PHP-FPM, সঙ্গে Redis অবজেক্ট ক্যাশ
ডকারে ওয়ার্ডপ্রেস নিয়ে বেশিরভাগ টিউটোরিয়াল "উঠে গেছে" পর্যন্ত এসেই থেমে যায়। এই লেখাটা আরেকটু দূর যায়: ভার্সন পিন করা, কম্পাইল করা PHP এক্সটেনশন দিয়ে সত্যিকারের অবজেক্ট ক্যাশ, এমন রিভার্স প্রক্সি যেটা সত্যিই TLS টার্মিনেট করে, আর অফিসিয়াল ইমেজের সেই দু-তিনটি আচরণ, যেগুলো প্রথম দিনে নয়, সমস্যায় ফেলে ত্রিশ দিনের মাথায়।
২ আগস্ট ২০২৬-এ যখন দেখেছি, তখন এগুলোই ছিল চলতি ভার্সন। পিন করে নিন। :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 | ঐচ্ছিক, ধাপ ৩ দেখুন |
| WP-CLI | wordpress:cli-php8.5 | সাইডকার, দরকার হলে চালাবেন |
ওয়ার্ডপ্রেস 7.1 আসছে ১৯ আগস্ট ২০২৬-এ, কাজেই ট্যাগের 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 সহ Apache, নাকি PHP-FPM। Apache মানে একটি ইমেজ, একটি প্রসেস, বাড়তি কনফিগ ফাইল নেই। 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হোস্টের nginx দিয়ে A দিয়েই শুরু করুন। এতে জিনিসপত্র সবচেয়ে কম, ভাঙার জায়গাও কম, আর সার্টিফিকেট নবায়নের কাজটা হোস্টের certbot নিজেই সেরে রাখে, আপনার মনে রাখতে হয় না। প্রতি লোকেশনে আলাদা ক্যাশিং বা রেট লিমিট দরকার হলে, কিংবা স্ট্যাটিক ফাইল এত বেশি সার্ভ করতে হলে যে সেগুলো PHP-র বাইরে রাখাটাই লাভজনক, তখন B-তে যাবেন।
১ থেকে ৫ নম্বর ধাপ সব ক্ষেত্রেই এক।
যা যা লাগবে
Compose v2 প্লাগইনসহ Docker Engine 27 বা তার পরের ভার্সন। লাগবে এটুকুই।
প্রজেক্টটা এমন জায়গায় রাখুন যেখানে nginx ঢুকতে পারে। হোস্টের nginx যদি সরাসরি ./data/wordpress পড়ে (পথ B), তাহলে /root/wp-তে রেখে লাভ নেই, যত chmod-ই করুন কাজ হবে না, কারণ /root-এর পারমিশনই 700। /srv/wp বা /opt/wp ব্যবহার করুন।
sudo mkdir -p /srv/wp && cd /srv/wpধাপ ১: কাঠামো
/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/wordpressRedis-এর জন্য আলাদা ডেটা ডিরেক্টরি নেই। কেন, সেটি ৪ নম্বর ধাপে।
ধাপ ২: .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 দুই সাইটে ভাগ করলে একে অন্যের ক্যাশ কী পড়তে শুরু করে। তখন সাইটে যা ঘটতে থাকে, তার কারণ খুঁজে বের করা রীতিমতো কঠিন হয়ে পড়ে।
ধাপ ৩: Redis ক্লায়েন্ট বেছে নেওয়া
এই ধাপটা বাদ দিলেও চলে। রাখলাম, কারণ PHP দুইভাবে Redis-এর সঙ্গে কথা বলতে পারে, আর অনেক গাইডেই কঠিন পথটাকে বাধ্যতামূলক বলে চালিয়ে দেওয়া হয়।
অফিসিয়াল ওয়ার্ডপ্রেস ইমেজে bcmath, exif, gd, intl, mysqli, zip আর imagick দেওয়া থাকে। Redis এক্সটেনশন থাকে না। তাতে অসুবিধা নেই। Redis Object Cache প্লাগইনের সঙ্গেই Predis আসে, খাঁটি PHP-তে লেখা একটি Redis ক্লায়েন্ট, আর এক্সটেনশন না পেলে প্লাগইন নিজে থেকেই সেটি ধরে নেয়।
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-র প্রতিটা সিকিউরিটি প্যাচের পর আপনাকেই আবার বিল্ড দিতে হবে। কী কিনছেন জেনে কিনুন।
লাগলে compose.yaml-এর পাশে একটি Dockerfile রাখুন।
Apache (ডেবিয়ান বেস, 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/pearAlpine-এ FPM (বিল্ড টুল আগে থেকে থাকে না, তাই যোগ করে কাজ শেষে ফেলে দিতে হয়):
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' দিয়ে ঠিক করেও দিতে পারেন।
ধাপ ৪: 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অনেকে এগুলো এনভায়রনমেন্ট ভেরিয়েবল হিসেবে পাঠিয়ে কনটেইনারের command বদলে ini ফাইলে লিখে নেয়। ওই পথে যাবেন না। command বদলানো মানে এখন আপনাকে এক লাইনের একটি শেল কমান্ড মেইনটেইন করতে হবে, যেটা শেষ হতেই হবে docker-entrypoint.sh apache2-foreground দিয়ে। ওখানে একটি টাইপো হলেই সেই এন্ট্রিপয়েন্টটা হারাবেন যেটা ওয়ার্ডপ্রেস খুলে বসায় আর wp-config.php লেখে। তার চেয়ে ফাইল মাউন্ট করা এক লাইনের YAML, আর প্রতিটা ইমেজ আপগ্রেডেও টিকে থাকে।
মাউন্টের নামে zz- থাকায় PHP ফাইলটা সবার শেষে পড়ে, ফলে ইমেজের নিজের opcache-recommended.ini-কে টপকে এটিই কার্যকর হয়।
ধাপ ৫: compose.yaml
এটি পথ A: Apache, লুপব্যাকে খোলা, সামনে হোস্টের 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/htmlnetworks: ব্লক নেই। Compose নিজেই সব সার্ভিসকে wp_default নামের একটি নেটওয়ার্কে বসায় আর প্রত্যেককে সার্ভিসের নামেই একটি DNS নাম দেয়। এ কারণেই WORDPRESS_DB_HOST হলো db:3306। এখানে হাতে নেটওয়ার্ক লিখে বাড়তি কিছু পাওয়া যায় না।
পাঁচটা জিনিস আলাদা করে বলে রাখি।
বাইন্ড অ্যাড্রেসটা ঐচ্ছিক নয়। শুধু "8080:80" লিখলে পোর্টটা সব ইন্টারফেসে খুলে যায়। Docker আবার আপনার নিয়মের আগেই নিজের iptables নিয়ম বসিয়ে রাখে, ফলে ufw নির্দ্বিধায় জানাবে ৮০৮০ বন্ধ আছে, অথচ পুরো ইন্টারনেট তখন আপনার ওয়ার্ডপ্রেস সাদা 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, অর্থাৎ ক্যাশ ভরে গেলে পুরনো কী চুপচাপ ফেলে না দিয়ে সে লেখাই বন্ধ করে দেয়, আর ওয়ার্ডপ্রেস তখন ক্যাশ এরর দিতে থাকে।
হেলথচেক চলে ১০ সেকেন্ড পরপর, ১২০ সেকেন্ড নয়। depends_on: service_healthy একটি চেক পাস করার অপেক্ষায় বসে থাকে, তাই ইন্টারভাল বড় রাখলে স্টার্টআপও তত ধীর হয়। দু-মিনিট ধরে স্ট্যাক ভাঙা দেখানোর মধ্যে কোনো কৃতিত্ব নেই।
পুরনো কোনো কনফিগ থেকে db_data ইত্যাদি ঘোষণা করা volumes: ব্লক টেনে এনে থাকলে সেটি মুছে দিন। ./data/db বাইন্ড-মাউন্ট করলে ওই নাম দেওয়া ভলিউমগুলো এমনিতেই অব্যবহৃত পড়ে থাকে।
ধাপ ৬: হোস্টে nginx
nginx আর certbot ইনস্টল করুন, তারপর একটি vhost বসিয়ে দিন:
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.comআসল হেডারটা হলো X-Forwarded-Proto। এটি দেখেই ওয়ার্ডপ্রেস ঠিক করে URL-এ http:// লিখবে না https://, আর অফিসিয়াল ইমেজের wp-config আগে থেকেই এটি পড়ে (দেখুন ধাপ ৮)। হেডারটা বাদ দিলে হয় মিক্সড-কনটেন্টের সতর্কবার্তা পাবেন, নয়তো রিডাইরেক্টের চক্করে পড়বেন।
"৩০ মেগাবাইটের ভিডিসেটি ৯০% গিয়ে আটকে যায়" এই সমস্যার সবচেয়ে বড় কারণ client_max_body_size আর post_max_size-এর গরমিল। PHP জিনিসটা দেখার আগেই nginx রিকোয়েস্ট বডি ফিরিয়ে দেয়, তাই শুধু PHP-র লিমিট বাড়িয়ে কোনো লাভ হয় না।
Certbot ফাইলটা নিজে থেকে বদলে 443 ব্লক আর একটি রিডাইরেক্ট যোগ করে দেয়, সঙ্গে নবায়নের টাইমারও বসায়। systemctl list-timers | grep certbot দিয়ে দেখে নিন।
ধাপ ৬, পথ B: PHP-FPM
কম্পোজ ফাইলে দুটি বদল। বিল্ডটা FPM-এর Dockerfile-এ তাক করান, আর HTTP-র বদলে FastCGI খুলুন:
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৯০০০ পোর্ট HTTP বলে না, বলে FastCGI। 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-এর লগেও কাজের কিছু থাকে না। ফাইল দুটিই আসলে এক, শুধু দুই জায়গায় দুই পথে বসানো, আর কোনটা কোনটার সঙ্গে মেলে সেটি একমাত্র আপনিই জানেন।
এই পথ নিয়ে ছোট দুটি কথা। PHP লোকেশনের ভেতরে try_files $uri =404 দিলে path-info দিয়ে কোড চালানোর সেই পুরনো ফাঁকটা বন্ধ হয়, যেটা ২০১৩ সালের কনফিগগুলো এখনো বয়ে বেড়াচ্ছে। আর এখানে HTTPS ধরার কায়দাটা আলাদা: রিভার্স প্রক্সিই নেই, তাই X-Forwarded-Proto-ও নেই। certbot TLS ব্লক বসিয়ে দিলে ডেবিয়ানের fastcgi_params সরাসরি HTTPS=on পাঠায়, ওয়ার্ডপ্রেস সেটি নিজেই বুঝে নেয়।
ধাপ ৬, বিকল্প: nginx কনটেইনার হিসেবে
হোস্টে nginx নেই, আর সবকিছু ডকারেই রাখতে চান, তাহলে এই পথ। চার নম্বর একটি সার্ভিস যোগ করুন আর 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 alreadyApache ইমেজ হলে config/nginx.conf হবে ধাপ ৬-এর প্রক্সি কনফিগটাই, শুধু 127.0.0.1:8080-এর জায়গায় proxy_pass http://wordpress:80;। FPM ইমেজ হলে হবে FastCGI কনফিগ, সঙ্গে fastcgi_pass wordpress:9000; আর root /var/www/html;। পথ নতুন করে মেলানোর দরকার নেই, কারণ ডকারের ভেতরে দুই কনটেইনারের কাছেই পথটা এক।
TLS নিয়ে এখানে ধোঁয়াশা রাখবেন না। কনটেইনারে nginx তখনই মানায়, যখন সার্টিফিকেট আগে থেকেই ডিস্কে আছে, কিংবা সাইটটা অফিসের ভেতরের আর সাদা HTTP-তেই চলে, কিংবা সামনে অন্য কিছু আপনার হয়ে TLS টার্মিনেট করছে। এই কনটেইনারের ভেতরে Let's Encrypt-এর অটো নবায়ন চালাতে গেলে লাগবে একটি certbot সাইডকার, শেয়ার করা ওয়েবরুট আর একটি রিলোড হুক। এতটা ঝামেলায় যেতে না চাইলে হোস্টের nginx ব্যবহার করুন, নয়তো এই সার্ভিসের বদলে Caddy বা Traefik নিন, ওরা ACME নিজেরাই সামলায়।
ধাপ ৭: প্রথম চালু
docker compose up -d
docker compose logs -f wordpressধাপ ৩-এ নিজের ইমেজ বেছে থাকলে তবেই আগে docker compose build চালাবেন।
খেয়াল রাখবেন দুটি লাইনের দিকে:
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.phpnginx-কে টানার আগে দেখে নিন স্ট্যাকটা লুপব্যাকে সাড়া দিচ্ছে কি না। কোন দিকটা গোলমাল করছে, সেটি ধরার এটিই দ্রুততম উপায়:
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এরপর ডোমেইনটা খুলে পাঁচ মিনিটের ইনস্টলেশনটা সেরে ফেলুন।
ধাপ ৮: ইমেজ wp-config.php-তে কী লেখে
দরকার পড়ার আগেই তিনটি কথা জেনে রাখা ভালো।
সল্ট একবারই তৈরি হয়। ফাইলটা যখন প্রথম বানানো হয়, তখনই /dev/urandom থেকে auth কী আর সল্ট এসে হুবহু ফাইলে বসে যায়। প্রতিবার চালু হওয়ার সময় এগুলো এনভায়রনমেন্ট থেকে পড়া হয় না। নিজের হাতে রাখতে চাইলে প্রথম চালু করার আগেই 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 হেডারটাই পাঠাচ্ছে না।
ধাপ ৯: অবজেক্ট ক্যাশ চালু করা
ইমেজ যেমন আছে তেমন রেখেই, তিনটা কমান্ড:
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 সার্ভিসের uid-টা শুধু সাজিয়ে রাখার জন্য নয়। CLI ইমেজটা Alpine-এর উপরে, সেখানে www-data-র uid ৮২। আর Apache-ওয়ালা ওয়ার্ডপ্রেস ইমেজ ডেবিয়ানের উপরে, সেখানে uid ৩৩। ভুল uid দিয়ে 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একদিন ট্রাফিক পাওয়ার পরও হিট রেট ৯০ শতাংশের নিচে থাকলে সেটি সাধারণত ওয়ার্ডপ্রেসের দোষ নয়, ক্যাশ থেকে কী বাদ পড়ে যাওয়ার সমস্যা। দেখুন used_memory maxmemory-তে গিয়ে আটকে আছে কি না, থাকলে REDIS_MAXMEMORY বাড়িয়ে দিন।
অবজেক্ট ক্যাশ আর পেজ ক্যাশ এক জিনিস নয়। এটি প্রতিটা রিকোয়েস্ট থেকে বারবার হওয়া ডেটাবেস কোয়েরিগুলো সরিয়ে দেয়, লগইন করা ব্যবহারকারী আর অ্যাডমিন প্যানেলের রিকোয়েস্টসহ। কিন্তু ওয়ার্ডপ্রেসকে PHP চালু করা থেকে ঠেকাতে পারে না। পুরো পেজ ক্যাশ করার কাজটি nginx বা আলাদা প্লাগইনের, আর সেটি সম্পূর্ণ আলাদা সিদ্ধান্ত।
ধাপ ১০: ব্যাকআপ
MariaDB চালু রেখে ডেটাডিরে tar চালাবেন না। আর্কাইভ বানানোর সময়ও ./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 কম্পোজ ফাইল দেখেই আবার বানিয়ে নেওয়া যায়। সল্টগুলোর ব্যাকআপ একবার নিয়ে রেখে দিন।
ধাপ ১১: হালনাগাদ
এই জায়গাটিতেই বেশিরভাগ মানুষ ধাক্কা খান। এন্ট্রিপয়েন্টের শর্তটা একবার দেখুন:
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 বিল্ড, নতুন Apache আর নতুন এক্সটেনশন পাবেন ঠিকই, কিন্তু ওয়ার্ডপ্রেসের কোর ফাইলগুলো যেখানে ছিল সেখানেই পড়ে থাকবে।
তাই আপডেট আসলে দুই ভাগে ভাগ হয়ে যায়:
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 --allMariaDB-র বড় ভার্সন বদলাতে MARIADB_AUTO_UPGRADE: "1" লাগে, উপরের কম্পোজ ফাইলে সেটি দেওয়াই আছে। পুরনো ডেটাডির পেলে এটি নিজেই mariadb-upgrade চালিয়ে নেয়। তবু আগে একটি ডাম্প নিয়ে রাখবেন, আর একবারে এক ভার্সন করে এগোবেন।
সমস্যা সমাধান
| উপসর্গ | সম্ভাব্য কারণ |
|---|---|
| প্রথম চালুতেই "Error establishing a database connection" | আগের কোনো দফায় ./data/db অন্য পাসওয়ার্ড দিয়ে তৈরি হয়েছিল। MariaDB ইমেজ কেবল ফাঁকা ডেটাডিরেই ক্রেডেনশিয়াল বসায়। হয় সেটি মুছে ফেলুন, নয়তো হাতে পাসওয়ার্ড রিসেট করুন |
| হোস্টের nginx থেকে 502 Bad Gateway | 127.0.0.1:8080-এ কিছুই শুনছে না। docker compose ps আর ss -ltnp | grep 8080 দেখুন |
| "Primary script unknown", পথ B | SCRIPT_FILENAME এখনো হোস্টের পথ দেখাচ্ছে। এটিকে /var/www/html$fastcgi_script_name করে দিন |
| স্ট্যাটিক ফাইলে nginx থেকে 403, পথ B | nginx ./data/wordpress পর্যন্ত যেতে পারছে না। 700 অনুমতির প্যারেন্ট ডিরেক্টরি থেকে প্রজেক্টটি সরান |
সাইট লোড হয় কিন্তু সব URL http:// | nginx X-Forwarded-Proto বসাচ্ছে না |
| অসীম HTTPS রিডাইরেক্ট | উপরেরটির মতোই কারণ |
| আপলোড সীমা এখনো 2M দেখাচ্ছে | ini মাউন্ট হয়নি, নয়তো /usr/local/etc/php/conf.d/-এর বাইরে মাউন্ট হয়েছে। docker compose exec wordpress php -i | grep upload_max দেখুন |
| আপলোড মাঝপথে থেমে যায় | nginx-এর client_max_body_size PHP-র post_max_size-এর চেয়ে ছোট |
| অবজেক্ট ক্যাশ চালুর পর সাদা পর্দা | পুরনো wp-content/object-cache.php। সেটি মুছে আবার wpcli redis enable চালান |
| প্লাগইন সক্রিয়, কিন্তু Redis-এ কিছুই নেই | ড্রপ-ইনটি কখনো লেখাই হয়নি। redis status বলবে ড্রপ-ইন বৈধ নয়। দেখুন wp-content-এ লেখা যায় কি না |
redis status বলছে "Not connected" | WP_REDIS_HOST ভুল, নয়তো কনটেইনারগুলো আলাদা কম্পোজ প্রজেক্টে আছে |
নিজস্ব ইমেজ বানিয়েছেন তবু redis status এখনো Predis বলছে | কনটেইনারটি এখনো পুরনো ইমেজেই চলছে। docker compose up -d --force-recreate wordpress |
| প্লাগইন ইনস্টলে FTP ক্রেডেনশিয়াল চাইছে | FS_METHOD direct-এ সেট করা নেই, নয়তো PHP ইউজার wp-content-এ লিখতে পারছে না |
| নতুন ইমেজ টেনেছেন, ওয়ার্ডপ্রেসের সংস্করণ বদলায়নি | এটিই প্রত্যাশিত। WP-CLI বা ড্যাশবোর্ড দিয়ে কোর হালনাগাদ করুন |
| ক্যাশ হিট রেট ৬০ শতাংশের আশেপাশে আটকে আছে | Redis maxmemory-তে পৌঁছে গিয়ে কী সরাচ্ছে। REDIS_MAXMEMORY বাড়ান |
এরপর কী করবেন
ধাপ ১০-এর ব্যাকআপ স্ক্রিপ্টটা ক্রনে বসিয়ে দিন, আর দরকার পড়ার আগেই একটি আলাদা ডিরেক্টরিতে রিস্টোর করে দেখে নিন কাজ করে কি না। তারপর মেপে দেখুন: Query Monitor ইনস্টল করে অবজেক্ট ক্যাশ বন্ধ রেখে সবচেয়ে ধীর পেজটার কোয়েরি সংখ্যা লিখে রাখুন, এরপর ক্যাশ চালু করে আবার দেখুন। এতক্ষণের পরিশ্রম কাজে লেগেছে কি না, তার একমাত্র প্রমাণ ওই সংখ্যাটাই।
এরপরের কাজটা হলো, সার্ভারের অবস্থা খারাপ হলে কোনো ভিজিটরের কাছ থেকে জানার আগেই নিজে টের পাওয়া। Prometheus, Node Exporter আর Grafana ঠিক এই ধরনের একটি সার্ভারেই সেই ব্যবস্থাটা করে দেয়। শুধু Node Exporter ড্যাশবোর্ড দেখেই বুঝে যাবেন Redis-এর মাপটা আসলে ঠিক আছে কি না।
সেলফ-হোস্টিং নিয়েই আমার বেশিরভাগ কাজ। গত দশ বছরে ক্লায়েন্টদের জন্য ৩০টিরও বেশি ওপেন সোর্স প্ল্যাটফর্ম বসিয়েছি আর চালিয়েছি। নিজে এসব ঝামেলায় যেতে না চাইলে একবার কথা বলুন।