Wordpress MU, NGINX and FastCGI on CentOS

Since my VPS at Loeniks is somewhat limited in resources, and because Apache has no trouble claiming all these resources, I took it upon myself to find an alternative. Lighttpd (lighty) and NGINX (engine-x) were both possible replacements. Both offered the ability to parse PHP scripts using a FastCGI implementation. Eventually I chose NGINX and went to work.

I was not very familiar with the FastCGI implementation of PHP and it was not easy finding information on how well WordPress would work within FastCGI. First I chose to set up NGINX as a reverse proxy between Apache and the rest of the world. NGINX easily allowed me to cache rendered pages for weeks, without Apache having to render them again. This allowed me to reduce Apaches memory usage a lot because I basicly only had to spawn two or three worker processes.

Eventually, it was time to say my goodbyes to Apache and let NGINX render all the pages using FastCGI. This howto assumes you already have NGINX and PHP installed.

Spawn-fcgi

Spawn-fcgi is a FastCGI daemon to which NGINX can send its PHP requests. Spawn-fcgi can be run locally or remotely, to handle PHP requests for multiple front-end servers. This howto however, will keep everything on one local machine, to keep things simple.

Install spawn-fcgi

We need the CLI version of PHP in order to allow spawn-fcgi to parse PHP scripts, install the CLI version of PHP if you have not done so already:

yum install spawn-fcgi php-cli

Configure spawn-fcgi

The startup configuration of the daemon is located at /etc/sysconfig/spawn-fcgi:
In my environment, I made the following settings:

OPTIONS="-u nginx -g nginx -a 127.0.0.1 -p 9000 -C 2 -F 1 -P /var/run/spawn-fcgi.pid -- /usr/bin/php-cgi"

The -u and -g flags allow you to set the user and group the daemon is to run as, these should match NGINX’s user and group.
The -a flag tells the daemon which IP address to bind to. If it is not set, spawn-fcgi will try to listen on all IP addresses, you don’t really want this. In a local setup, loopback (127.0.0.1) is enough.
The -p flag tells the daemon which port it should listen on, you can choose any port you want. 9000 was not in use on my server, and it sounded just right.
The -C flag tells the daemon how many children should be spawned. On busy servers, you’ll probably want to set this higher than 2, otherwise NGINX will soon prompt an error page that not all needed resources are available.
The -F flag was confusing, it sets how many forks should be done. I think this is for multicore-systems (which my VPS is not).
The -P flag is for the location of the pidfile.

Start spawn-fcgi

The spawn-fcgi daemon should be able to start without errors or warnings now:

service spawn-fcgi start

Now set spawn-fcgi to start at boot-time:

chkconfig spawn-fcgi on

NGINX

I chose to make my NGINX configuration somewhat modular. Extra functionality and virtualhosts will be loaded through includes. This gives me more configuration files to keep track of, but in the long run, I find it easier to deal with in this manner.

The main configuration file: /etc/nginx/nginx.conf

These settings work well for me. If you think some settings should be different, be my guest and tell me what I am doing wrong ;)

user nginx; worker_processes 2;
pid /var/run/nginx.pid;
events { 
  worker_connections 1024;
} 

http {
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  
  access_log /var/log/nginx/access.log;
  log_format cache '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent"' ' *** $upstream_cache_status ' 'Cache-Control: $upstream_http_cache_control ';
  error_log /var/log/nginx/error.log;
  
  server_names_hash_bucket_size 64;
  
  sendfile on;
  tcp_nodelay on;
  tcp_nopush on;
  
  keepalive_timeout 65;
  
  # Load extra configuration files, if any
  include /etc/nginx/conf.d/*.conf;
  
  # Load active virtual hosts, if any
  include /etc/nginx/vhosts.d/*.conf;
}

Extra include: /etc/nginx/conf.d/gzip.conf

This file is loaded by the line include /etc/nginx/conf.d/.conf;*in nginx.conf. It gives us some default compression settings.

gzip on;
gzip_comp_level 5;
gzip_http_version 1.0;
gzip_min_length 0;
gzip_types text/plain text/css image/x-icon application/x-javascript;
gzip_vary on;

Virtualhost include: /etc/nginx/vhost.d/www_eelcowesemann_nl.conf

This configuration is loaded by the line include /etc/nginx/vhost.d/.conf;* in nginx.conf. It holds the settings for www.eelcowesemann.nl. Because I use WordPress-MU, I can share settings with other virtualhosts. These settings are included by the line include /etc/nginx/wpmu_defaults.conf;.
Furthermore, I have two listen lines, the first is for IPv4, the second for IPv6.

server {
  # IPv4 
  listen 83.96.227.27:80; 
  # YAY IPv6 \o/
  listen [2001:828:101:20::8]:80;
  
  server_name www.eelcowesemann.nl;
  
  access_log /var/log/nginx/www.eelcowesemann.nl_log cache;
  error_log /var/log/nginx/www.eelcowesemann.nl_err;
  
  # Load default wordpress-mu settings
  include /etc/nginx/wpmu_defaults.conf;
}

If you don’t mind all your logs to be put into one and the same logfile, you can choose to list all your sites behind server_name. I like having separate logfiles, so I chose to make a configuration for each site.

WordPress-MU configuration: /etc/nginx/wpmu_defaults.conf

This file holds all settings that enables both PHP and the rewrite rules for WordPress-MU.
The line try_files $uri $uri/ /index.php?q=$uri&$args; first tries to access the files directly on the filesystem. If this fails, the URI is handed to index.php.
The location ~ .php$ {} block makes sure that PHP files are sent to FastCGI to be parsed.
“Page not found” errors (error 404) are also pushed through index.php, so WordPress can make it look pretty.
Files that have been uploaded in WordPress are saved in a different location. In order to enable WordPress to find the files again we need the rewrite ^./files/(.) /wp-includes/ms-files.php?file=$1 last; rewrite rule.
The location ~ .(js|css|png|jpg|jpeg|gif|ico)$ {}* block tells browsers to cache these files as long as possible.

charset utf-8;

root /var/www/sites/wordpress-mu/html;
index index.php;

# Allow WordPress to find uploaded files
rewrite ^.*/files/(.*) /wp-includes/ms-files.php?file=$1 last;
location / { 
  try_files $uri $uri/ /index.php?q=$uri&$args;
  
  if (!-e $request_filename) {
    rewrite ^.+/?(/wp-.*) $1 last;
    rewrite ^.+/?(/.*\.php)$ $1 last;
    rewrite ^(.+)$ /index.php?q=$1 last;
  }
}

# enable browser caching for static files
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
  expires max;
}

# send PHP files to be handled by FastCGI
location ~ \.php$ {
  fastcgi_pass 127.0.0.1:9000;
  fastcgi_index index.php;
  fastcgi_param SCRIPT_FILENAME /var/www/sites/wordpress-mu/html$fastcgi_script_name;
  include /etc/nginx/fastcgi_params;
}

# Let WordPress handle 404 errors
error_page 404 = /index.php?q=$uri;
error_page 500 502 503 504 /50x.html;
location = /50x.html { 
  root /usr/share/nginx/html;
}

Start NGINX

Test your configuration with the following command:

nginx -t

If your configuration doesn’t contain errors, you should get the following output:

the configuration file /etc/nginx/nginx.conf syntax is ok configuration file /etc/nginx/nginx.conf test is successful

You can now start NGINX:

service nginx start

Make sure NGINX starts at boot time:

chkconfig nginx on

Finally

This NGINX setup works very well for me, you are now reading this howto because of it ;-). Switching from Apache to NGINX more or less halved the memory usage on my server: the first step of setting up NGINX as a reverse proxy already freed up a lot of memory. Eventually stopping Apache all together enabled me to squeeze just a little more out of my VPS. I’m not sure yet how well FastCGI will hold out when it gets a bit busier on my sites, but I am confident that I will not be using Apache in environments with limited resources anymore.

If you have anything to add, be it questions, remarks or whatever, feel free to leave a message.