Scaling WordPress Multisite on AlmaLinux VPS

Running WordPress Multisite on a single VPS works fine at 5 sites, then 20, and one day you notice the admin dashboard crawls and the database is pegging the CPU. Scaling a multisite network is not the same as scaling a single WordPress install. You need to control PHP-FPM pools, separate the database load, add an object cache, and decide between subdomains and subdirectories early because that choice is painful to reverse. This guide covers the architecture and the concrete AlmaLinux 9 commands to keep a multisite network fast from the first few sites through a few hundred.
Prerequisites
- An AlmaLinux 9 VPS with root or sudo access, at least 4 GB of RAM for the setup in this guide (a Linux VPS with 8 GB is more comfortable for 50+ sites).
- A registered domain with the ability to add DNS records (A records and a wildcard if you plan subdomains).
- Nginx and PHP-FPM already installed, or the willingness to install them as part of this guide.
- Comfort with the command line, SSH, and editing files inside
/etc/nginx/and/etc/php-fpm.d/.
Why multisite changes your scaling math
A WordPress Multisite network shares one codebase, one database, and one set of plugins and themes across all sites. That is its appeal: update the core once, activate a plugin network-wide, and manage every site from a single dashboard. But sharing a database means one slow site or one runaway query can take down the whole network. The shared codebase also means object caching becomes far more important, because the same theme files and options rows are read over and over by every site.
The architecture decisions you make here matter more than any individual server tweak. Subdomain installs can be pointed at different servers later. Subdirectory installs are tied to the domain structure and much harder to split across machines. Object caching stops a site with 200 transient options from hammering the database on every page load. And PHP-FPM pool tuning decides whether 30 simultaneous requests to different sites starve the VPS or get served from separate worker pools. Get those three right and you can run a surprisingly large network on a single AlmaLinux VPS.
多站点网络共享一个数据库,慢查询会影响所有站点。
A multisite network shares one database, so a single slow query affects every site.
Step 1 - Decide the site structure before you install
WordPress gives you three choices during installation: subdomains (site1.example.com), subdirectories (example.com/site1), or a domain mapping plugin for fully custom domains. Subdomains are the right choice for most multisite networks, and here is why. The WordPress network admin dashboard lists every site regardless of structure, but subdomains let you later separate sites onto different VPS boxes by changing a DNS record and an nginx server block. A subdirectory site is bound to the primary domain's document root and cookie handling, which makes splitting it out later a messy migration.
Subdirectories also cause a subtle problem with caching. Page cache keys and CDN URLs must account for the site path, so a misconfigured cache serves one site's HTML to another. Subdomains keep the cache key simple: the hostname. If you need custom domains later, a plugin like WordPress MU Domain Mapping can overlay them on a subdomain structure, but it does not help you split a subdirectory network.
For the rest of this guide I assume a subdomain install. Set the primary domain DNS first: an A record for example.com and a wildcard *.example.com pointing at your VPS IP. The wildcard is what makes new subsites work without touching DNS every time. Most DNS providers support wildcard A records, and your registrar or DNS panel is where you add them. WordPress will not create the DNS record for you.
Step 2 - Install and configure the stack on AlmaLinux 9
AlmaLinux 9 ships with PHP 8.0 in the default repositories, which is too old for current WordPress. Enable the Remi repository to get PHP 8.3, the version WordPress supports today.
sudo dnf -y install epel-release
sudo dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf -y module reset php
sudo dnf -y module enable php:remi-8.3
sudo dnf -y install nginx php-fpm php-mysqlnd php-gd php-xml php-mbstring php-curl php-zip php-intl php-redis php-opcache
The php-redis package is the client library WordPress uses to talk to a Redis server, which we set up next. Install MariaDB for the database and Redis for object caching.
sudo dnf -y install mariadb-server redis
sudo systemctl enable --now mariadb redis
sudo systemctl enable --now nginx php-fpm
Secure the database with the standard mysql_secure_installation script. You can skip the validate password plugin if you already run strong passwords, but do set a root password and remove anonymous users.
sudo mysql_secure_installation
At this point, verify each service is running before moving on. A silent failure here will waste time later.
systemctl status nginx php-fpm mariadb redis | grep -E "Active:|●"
Expect four active (running) lines. If any service failed, check the journal before continuing.
sudo journalctl -u php-fpm -u nginx -u mariadb -u redis --no-pager -n 30
Step 3 - Tune PHP-FPM for the multisite workload
Default PHP-FPM settings assume one site per server. Multisite changes that: each site can generate concurrent requests, and they all share the same php-fpm pool. The biggest mistake is running everything in the default www.conf pool with static workers sized for one site. A multisite network needs per-directory pools or at least a pool tuned for many concurrent short requests.
For a VPS with 8 GB of RAM, I use a dynamic pool capped at 20 workers with a generous request limit. This is the config in /etc/php-fpm.d/www.conf:
pm = dynamic
pm.max_children = 20
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500
request_terminate_timeout = 60s
Set pm.max_requests = 500. This tells PHP-FPM to recycle a worker after 500 requests, which prevents memory leaks from plugins accumulating over days of uptime. Without it, a plugin that leaks 2 MB per request turns a 40 MB worker into a 400 MB zombie after a few hours on a busy site.
The real win for multisite is separating the admin traffic from the front-end. The admin dashboard and the public site have completely different performance profiles. I created a second pool called admin.conf with fewer workers so an admin session cannot starve public visitors:
sudo cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/admin.conf
sudo sed -i 's/\[www\]/[admin]/' /etc/php-fpm.d/admin.conf
sudo sed -i 's/listen = \/run\/php-fpm\/www.sock/listen = \/run\/php-fpm\/admin.sock/' /etc/php-fpm.d/admin.conf
sudo sed -i 's/user = nginx/user = nginx/' /etc/php-fpm.d/admin.conf
Then edit admin.conf and lower the pool size to 6 workers. The nginx config uses this pool only for /wp-admin and /wp-login.php.
location ~ ^(/[^/]+)?(/wp-admin|/wp-login\.php)(/.*)?$ {
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm/admin.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Verify the new pool starts cleanly.
sudo php-fpm -t
sudo systemctl restart php-fpm
ps aux | grep php-fpm | grep admin
Step 4 - Configure nginx for multisite path and cookie rules
Multisite needs one nginx trick that a single-site install does not: the SUB_DOMAIN handling for the uploads path. WordPress stores uploaded files for each subsite in /wp-content/blogs.dir/ on older installs or in /wp-content/uploads/sites/<id>/ on modern ones, and the request URI always begins with the site's path, which for subdomains is just /. The important part is passing the correct SCRIPT_FILENAME so PHP can route the request to the right subsite.
The core nginx server block for the primary domain looks like this:
server {
listen 80;
server_name example.com *.example.com;
root /var/www/multisite;
index index.php;
# Multisite subdirectory rewrite rule (needed even for subdomain installs)
if (!-e $request_filename) {
rewrite ^/(wp-.*.php)$ /$1 last;
rewrite ^(/[^/]+)?(/wp-.*) $2 last;
rewrite ^(/[^/]+)?(/.*\.php)$ $2 last;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm/www.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Deny access to sensitive files
location ~ ^/wp-content/uploads/.*\.php$ { deny all; }
location = /wp-config.php { deny all; }
}
With a wildcard server name, nginx handles every new subsite automatically, no config reload needed. Add HTTP/2 and a TLS certificate with Certbot so every subsite is served over HTTPS. Redirect HTTP to HTTPS at the server level before tuning caching, otherwise you will cache mixed content.
sudo dnf -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d '*.example.com'
Step 5 - Add Redis object caching
Object caching is where multisite scaling either works or collapses. Every subsite reads the same plugin and theme options, and on a network of 50 sites that is hundreds of duplicate database queries per page load. Redis holds those rows in memory so the database only sees the misses.
Install and start Redis with persistence enabled, then point WordPress at it with the Redis Object Cache plugin or a drop-in. The plugin is simpler to manage across the network because updates roll out from the network admin.
sudo systemctl edit redis
Set maxmemory to a sane value for your VPS. I set 512 MB on a 4 GB box, 1 GB on an 8 GB box. The config override is in /etc/systemd/system/redis.service.d/override.conf:
[Service]
ExecStart=
ExecStart=/usr/bin/redis-server /etc/redis.conf --maxmemory 1gb --maxmemory-policy allkeys-lru
sudo systemctl daemon-reload
sudo systemctl restart redis
redis-cli ping
The redis-cli ping should return PONG. Then install the Redis Object Cache plugin from the network plugins screen, activate it network-wide, and click "Enable Object Cache" on the settings page. Verify it works by checking Redis memory usage:
redis-cli info memory | grep used_memory_human
redis-cli dbsize
A healthy network shows a growing dbsize after a few page loads across different subsites. If the number stays near zero, the drop-in was not written, usually because the filesystem where wp-content lives does not allow PHP to write files.
Step 6 - Separate the database from the web head
At some point, the database becomes the bottleneck, not PHP. On a multisite with 100+ sites and active comment sections, you will see mysqld pegging a core while nginx sits idle. That is the signal to move MariaDB onto its own machine.
When you separate the database, update wp-config.php on the web head to point at the remote host and create a database user that only allows connections from the web server's IP:
CREATE USER 'wpuser'@'203.113.xx.xx' IDENTIFIED BY 'strongpassword';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'203.113.xx.xx';
FLUSH PRIVILEGES;
In wp-config.php, define the host explicitly to avoid DNS lookups on every connection:
define('DB_HOST', '192.168.1.10');
Update the MariaDB config on the database server to listen on the private interface and skip reverse DNS lookups:
sudo sed -i '/\[mysqld\]/a bind-address=0.0.0.0\nskip-name-resolve' /etc/my.cnf.d/mariadb-server.cnf
sudo systemctl restart mariadb
Use a private IP if your VPS provider offers one. thueVPS does not expose an internal network between VPSes, so a public IP with a MariaDB user restricted to your web server's address is the practical route. Firewalld must allow MySQL on the database server:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.113.xx.xx" port protocol="tcp" port="3306" accept'
sudo firewall-cmd --reload
Verify the web server can reach the database:
mysql -h 192.168.1.10 -u wpuser -p -e 'SELECT 1;'
Troubleshooting
Subsites return 404 on the homepage. Usually the nginx rewrites are wrong or the permalink structure was not flushed. Fix: go to Settings → Permalinks on the primary site and save. If that does not help, check the nginx error log.
sudo tail -20 /var/log/nginx/error.log
Redis object cache turns off after every page load. The drop-in file is not being written or is being deleted by a security plugin. Fix: verify the wp-content directory is writable by the nginx user and check for a plugin that clears advanced cache on every request.
sudo -u nginx touch /var/www/multisite/wp-content/object-cache.php && ls -la /var/www/multisite/wp-content/object-cache.php
PHP-FPM workers spike and the VPS runs out of memory. A single subsite with a bad plugin is consuming all workers. Fix: lower pm.max_children until each worker has enough RAM, then identify the site with the highest traffic in the network admin and investigate its plugins.
free -m && ps aux --sort=-%mem | head -15
FAQ
How much RAM does a WordPress Multisite VPS need?
A small network of 5-10 sites runs on 2 GB, but 50+ sites with Redis and a reasonable buffer pool want 4-8 GB. The database and PHP workers together consume 1.5-2 GB before any traffic arrives.
Subdomains or subdirectories for WordPress Multisite?
Subdomains. They allow you to split sites onto different servers later, keep cache keys simple, and avoid the path-based cookie issues that subdirectory installs cause.
Why does my multisite database lock up under traffic?
Too many concurrent queries hitting InnoDB tables without object caching. Enable Redis first, then consider moving the database to a separate VPS and tuning MariaDB's buffer pool to 50-60% of available RAM.
Can I serve hundreds of sites on one VPS?
Yes, if the sites are low-to-moderate traffic and you properly tune PHP-FPM, enable Redis, and use a page cache like Nginx FastCGI Cache or a plugin with LSCache. High-traffic sites need their own machines.
Does the wildcard DNS record cost extra?
No, wildcard A records are a standard DNS feature. You add one record (*.example.com) pointing to the same VPS IP as the primary domain.
Related articles
- How much RAM does a WordPress VPS need in 2026
- Best VPS config for a WooCommerce store in 2026
- Optimize PHP-FPM for WordPress on a VPS
- Set up Redis cache for web applications on a Linux VPS
WordPress 多站点扩展要点
在 AlmaLinux 9 VPS 上扩展 WordPress 多站点,关键是提前决定子域名结构、调整 PHP-FPM 池、启用 Redis 对象缓存,并在数据库成为瓶颈时将其分离到独立服务器。子域名比子目录更易扩展,缓存键也简单。将 Redis 最大内存设为 VPS 内存的八分之一到四分之一,max_requests 设为 500,可防止内存泄漏。超过 100 个站点时,将 MariaDB 移到独立 VPS 并限制数据库用户只允许 Web 服务器 IP 连接。


