WordPress.org

Support

Support » Multisite » [Resolved] nginx php-fpm PHP APC WordPress multisite (subdirectory) WP Super Cache

[Resolved] nginx php-fpm PHP APC WordPress multisite (subdirectory) WP Super Cache

  • Hooray! I’ve got WordPress multisite (subdirectory) working with nginx 0.8.54 with the static gzip module, php-fpm (FastCGI Process Manager – PHP 5.3.5), PHP APC (opcode cache – 3.1.6 or 3.1.7), and WP Super Cache (latest dev). I also made my nginx/php-fpm configuration generic enough that most people should be able to copy and paste and have it work for them as well – WITHOUT security vulnerabilities or other major issues. I spent a good week on planning the configuration and then a few hours working out the bugs. I had significant help from people on the nginx IRC channel. Serious props to those good folks!

    If you don’t know what nginx is, it is an alternate web/proxy server to Apache that is gaining in popularity. If you don’t know what php-fpm is, it is a server whose sole purpose in life is to serve up PHP responses to FastCGI requests made by a web server. At any rate, I’ve broken up the nginx configuration into five distinct files and attempted to heavily comment them to make each option easier to understand. I also made a best-effort attempt to follow “best practices” for nginx configurations. This configuration works but could use some minor tweaks. First up is /etc/nginx/nginx.conf:

    # Generic startup file.
    user {user} {group};
    worker_processes  3;
    
    error_log  /var/log/nginx/error.log;
    pid        /var/run/nginx.pid;
    
    # Keeps the logs free of messages about not being able to bind().
    #daemon     off;
    
    events {
    	worker_connections  1024;
    }
    
    http {
    #	rewrite_log on;
    
    	include mime.types;
    	default_type       application/octet-stream;
    	access_log         /var/log/nginx/access.log;
    	sendfile           on;
    #	tcp_nopush         on;
    	keepalive_timeout  3;
    #	tcp_nodelay        on;
    #	gzip               on;
    	index              index.php index.html index.htm;
    
    	# Upstream to abstract backend connection(s) for PHP.
    	upstream php {
            	server unix:/tmp/php-fpm.sock;
    #       	server 127.0.0.1:9000;
    	}
    
    	include sites-enabled/*;
    }

    Now, you’ll observe that this is a bit different from most nginx.conf files. I opted to follow the Ubuntu/Debian method of declaring enabled sites for maximum flexibility – using ‘sites-available’ to store a config and then symlink to the config file from ‘sites-enabled’. Here’s my site configuration:

    # Redirect everything to the main site.
    server {
    	listen 80 default;
    	server_name *.mysite.com;
    	rewrite ^ http://mysite.com$request_uri permanent;
    }
    
    server {
    	server_name mysite.com;
    	root /var/www;
    
    	include global/restrictions.conf;
    	include global/wordpress-ms-subdir.conf;
    }

    If you look around the Internet, you can find various WordPress configs for nginx. Most of them drop stuff into the ‘server’ block, but I figure some people might want to reuse the same logic over and over. As long as the blog is in the root of the site, this is no problem. I created a ‘global’ subdirectory (/etc/nginx/global/) to add extra rules like WP stuff. Here’s ‘global/restrictions.conf’:

    # Global restrictions configuration file.
    # Designed to be included in any server {} block.</p>
    location = /favicon.ico {
    	log_not_found off;
    	access_log off;
    }
    
    location = /robots.txt {
    	allow all;
    	log_not_found off;
    	access_log off;
    }
    
    # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
    location ~ /\. {
    	deny all;
    	access_log off;
    	log_not_found off;
    }

    And now for the basic WordPress rules in ‘global/wordpress-ms-subdir.conf’:

    # WordPress multisite subdirectory rules.
    # Designed to be included in any server {} block.
    
    # This order might seem weird - this is attempted to match last if rules below fail.
    # http://wiki.nginx.org/HttpCoreModule
    location / {
    	try_files $uri $uri/ /index.php?$args;
    }
    
    # Add trailing slash to */wp-admin requests.
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;
    
    # Directives to send expires headers and turn off 404 error logging.
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
    	expires 24h;
    	log_not_found off;
    }
    
    # Pass uploaded files to wp-includes/ms-files.php.
    rewrite /files/$ /index.php last;
    
    # For multisite:  Use a caching plugin/script that creates symlinks to the correct subdirectory structure to get some performance gains.
    set $cachetest "$document_root/wp-content/cache/ms-filemap/${host}${uri}";
    if ($uri ~ /$) {
    	set $cachetest "";
    }
    if (-f $cachetest) {
    	# Rewrites the URI and stops rewrite processing so it doesn't start over and attempt to pass it to the next rule.
    	rewrite ^ /wp-content/cache/ms-filemap/${host}${uri} break;
    }
    
    if ($uri !~ wp-content/plugins) {
    	rewrite /files/(.+)$ /wp-includes/ms-files.php?file=$1 last;
    }
    
    # Uncomment one of the lines below for the appropriate caching plugin (if used).
    # include global/wordpress-ms-subdir-wp-super-cache.conf;
    # include global/wordpress-ms-subdir-w3-total-cache.conf;
    
    # Rewrite multisite '.../wp-.*' and '.../*.php'.
    if (!-e $request_filename) {
    	rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last;
    	rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ $1 last;
    }
    
    # Pass all .php files onto a php-fpm/php-fcgi server.
    location ~ \.php$ {
    	# Zero-day exploit defense.
    	# http://forum.nginx.org/read.php?2,88845,page=3
    	# Won't work properly (404 error) if the file is not stored on this server, which is entirely possible with php-fpm/php-fcgi.
    	# Comment the 'try_files' line out if you set up php-fpm/php-fcgi on another machine.  And then cross your fingers that you won't get hacked.
    	try_files $uri =404;
    
    	fastcgi_split_path_info ^(.+\.php)(/.+)$;
    	include fastcgi_params;
    	fastcgi_index index.php;
    	fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    #	fastcgi_intercept_errors on;
    	fastcgi_pass php;
    }

    For WP Super Cache, the ‘global/wordpress-ms-subdir-wp-super-cache.conf’ file should look like this:

    # WP Super Cache rules.
    # Designed to be included from a 'wordpress-ms-...' configuration file.
    
    # Enable detection of the .gz extension for statically compressed content.
    # Comment out this line if static gzip support is not compiled into nginx.
    gzip_static on;
    
    set $supercacheuri "";
    set $supercachefile "$document_root/wp-content/cache/supercache/${http_host}${uri}index.html";
    if (-e $supercachefile) {
    	set $supercacheuri "/wp-content/cache/supercache/${http_host}${uri}index.html";
    }
    
    # If this is a POST request, pass the request onto WordPress.
    if ($request_method = POST) {
    	set $supercacheuri "";
    }
    
    # If there is a query string, serve the uncached version.
    if ($query_string) {
    	set $supercacheuri "";
    }
    
    # Logged in users and those who have posted a comment get the non-cached version.
    if ($http_cookie ~* comment_author_|wordpress_logged_in|wp-postpass_) {
    	set $supercacheuri "";
    }
    
    # Mobile browsers get the non-cached version.
    # Wastes CPU cycles if there isn't a mobile browser WP theme for the site.
    if ($http_x_wap_profile) {
    	set $supercacheuri "";
    }
    
    if ($http_profile) {
    	set $supercacheuri "";
    }
    
    if ($http_user_agent ~* (2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800)) {
    	set $supercacheuri "";
    }
    
    if ($http_user_agent ~* (w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-)) {
    	set $supercacheuri "";
    }
    
    # Stop processing if the supercache file is valid.
    if ($supercacheuri) {
    	rewrite ^ $supercacheuri break;
    }

    If you don’t have the static gzip module compiled in, be sure to comment out the correct line.

    If you are feeling adventuresome and want to squeak out a little tiny bit more performance, use a custom script like this and run it (and needs to be run after each new blog is created after that):

    <?php
    	require_once "wp-load.php";
    
    	@ini_set('display_errors', 'On');
    	nocache_headers();
    
    	$blogs = $wpdb->get_results($wpdb->prepare("SELECT blog_id, domain, path FROM " . $wpdb->blogs . " WHERE site_id = %d AND public = '1' AND archived = '0' AND mature = '0' AND spam = '0' AND deleted = '0' AND blog_id <> 1 AND last_updated <> '0000-00-00 00:00:00'", $wpdb->siteid));
    	if ($blogs)
    	{
    		// Generate new symbolic links for uploaded files for each blog.
    		foreach ($blogs as $blog)
    		{
    			$path = "/path/to/root/wp-content/cache/ms-filemap/" . $blog->domain;
    			if (!is_dir($path))  @mkdir($path, 0777, true);
    			$path .= $blog->path;
    			$path = substr($path, 0, -1);
    			if (!is_dir($path))  symlink("/path/to/root/wp-content/blogs.dir/" . $blog->blog_id . "/", $path);
    		}
    	}
    ?>

    The PHP script generates symbolic links from the names of the blogs to the numeric (ID) representation of each blog. Then, nginx doesn’t pass requests for uploaded files to php-fpm. If you don’t plan on doing this, comment out the relevant rules to save a few CPU cycles per request. Also, if you don’t have a mobile site WP theme, comment out the relevant WP Super Cache rules to save a lot of CPU cycles.

    A couple last notes: This whole setup assumes that the root of the site is the blog and that all files that will be referenced reside on the host. If you put the blog in a subdirectory such as /blog, then the rules will unfortunately have to be modified to handle that situation. Perhaps someone can take these rules and make it possible to, for instance, use a:

    set $wp_subdir “/blog”;

    Directive in the main ‘server’ block and have it automagically apply to the generic WP rules.

    I’m also considering splitting the WordPress config into ‘precache’ and ‘postcache’ pieces so that I can inject additional custom rules if I want to either override the cache or to just do custom WP stuff before the \.php$ line.

    I didn’t write a W3 Total Cache equivalent for the WP Super Cache. If someone does create one, put it in this thread or create a new thread and reference this thread.

    Also, these rules likely won’t work for multisite in subdomain mode.

    Finally, if you are in the market for nginx, you are probably running into scaling issues. You might want to look at various articles on scaling WordPress like:

    http://weblogtoolscollection.com/archives/2010/03/27/scaling-wordpress-part-1-using-mysql-replication-and-hyperdb/

    nginx and php-fpm only give a moderate boost in performance, so you might not like the results. PHP APC and WP Super Cache (or any of the other caching plugins) have much more impressive results.

    I’m kind of hoping the WP folks take note of this thread and integrate this approach into the WP core installer.

Viewing 15 replies - 1 through 15 (of 58 total)
  • Great post! Thank you! I don’t think it’s worth making Supercache even more complicated by trying to automate this but I’ll link to this post in the readme.txt

    You’re welcome! I agree with your position on automation of nginx rules in WP Super Cache. Especially since nginx configurations are usually only able to be modified by the superuser. The easiest way to make sure everything syncs up is to compare the .htaccess rules the plugin generates with the rules in the nginx configuration and then manually make any changes as necessary. The similarities are fairly obvious but documentation on the Rewrite module may help anyone needing them:

    http://wiki.nginx.org/HttpRewriteModule

    I forgot to one thing in my original post. The first two lines of nginx.conf are:

    user {user} {group};
    worker_processes  3;

    The ‘user’ line is the user and group to run the nginx as (e.g. ‘user www-data www-data’). The ‘worker_processes’ line should relate to how many CPUs to occupy/dedicate to nginx. I’m experimenting with 3, but most people set it to 2. Don’t set it to more than the total number of processors available.

    Obviously, this configuration is still a little bit of a work in progress but it does work and is running smoothly on a production server.

    This post is so great. but any good tutorial how to set nginx + php-fpm + PHP APC environment? I tried a lot but still have some problems ­čÖü

    @sparanoid – Getting nginx, php-fpm, APC, and WP Super Cache working together really depends on the environment. If you can get just APC and WP Super Cache working, you are going to see drastic improvements to performance. nginx and php-fpm are only going to add marginal improvement and require a lot more effort to set up than it might be worth.

    APC can be installed in module form (.so) via PECL directly on most Linux hosts. PECL is like PEAR but for extensions to PHP. This post should make installing APC easier:

    http://www.agileapproach.com/blog-entry/howto-install-pecl-apc-cache-centos-without-xampp

    While the PECL approach results in a slightly slower APC build than compiling it directly into the PHP binary, the actual difference is negligible and results in far fewer headaches. Well, fewer headaches as long as it works the during first attempt to compile APC.

    If you are using Mac OS X Server, PECL won’t likely work or won’t generate correct binaries. Apple is always lagging behind anyway with their third-party server software, but you’ll have to drop Apple Apache and Apple PHP if you want to use APC. MacPorts makes it easy to build a version of PHP that works with APC, but you’ll also need to use MacPorts Apache as the version of PHP MacPorts builds will crash Apple Apache for some reason. Building a MacPorts php-fpm is rather difficult at the moment and requires editing portfiles to add a ‘fpm’ “variant” to the PHP build process. MacPorts has a nginx build though that uses the latest source code.

    If you really want to go down the nginx + php-fpm route, these instructions are useful as general-purpose guidelines that you’ll need to adapt to your specific setup:

    http://blogs.sitepoint.com/2010/11/19/lightning-fast-wordpress-with-php-fpm-and-nginx/

    If you build one (nginx or php-fpm), you might as well build the other as they work rather well together. Skip those nginx configuration directions and use the ones here and you probably already have WP installed or know how to do it, so skip those directions too. Building nginx from source is the best way to go because most repositories are generally several versions behind. Building PHP from source with the –enable-fpm option is really the only way to get a php-fpm binary. The “you have to build from source” annoyance will hopefully change in the future.

    Minor correction needed. If you use weird permalinks or use plugins with AJAX callbacks, you might need to modify:

    rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last;

    To be more like:

    rewrite ^/[_0-9a-zA-Z-]+(/[_0-9a-zA-Z-]+)?(/wp-.*) $2 last;

    OR, what I ended up doing:

    # Rewrite multisite '.../wp-.*' and '.../*.php'.
    if (!-e $request_filename) {
    	rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last;
    	rewrite ^/[_0-9a-zA-Z-]+.*(/wp-admin/.*\.php)$ $1 last;
    	rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ $1 last;
    }

    To handle URLs like:

    http://mywebsite.com/secondblog/archives/wp-admin/admin-ajax.php

    IMO, the latter approach seems cleaner. The only real issue is AJAX, so the second approach solves the problem a bit more cleanly.

    Another minor modification. URLs like http://mysite.com./ will create a new WP Super Cache directory called “mysite.com.” (note the extra ‘.’). The server configuration rules need to be changed to:

    server {
    	server_name *.mysite.com;
    	root /var/www;
    
    	# Redirect everything to mysite.com
    	if ($http_host != "mysite.com") {
    		rewrite ^ http://mysite.com$request_uri permanent;
    	}
    
    	include global/restrictions.conf;
    	include global/wordpress-ms-subdir.conf;
    }

    This is actually a little simpler than the previous file but technically slightly slower due to the ‘if’ statement since ‘if’ is part of the nginx ‘rewrite’ module. If you don’t care about ‘mysite.com.’ (possibly a SEO issue) being redirected or you garbage collect cached files, then the former approach will work just fine.

    Only slightly related. I needed SCRIPT_URI to be passed to PHP. So I ended up adding:

    fastcgi_param  SCRIPT_URI         $scheme://$host$request_uri;

    To the ‘/etc/nginx/fastcgi_params’ file. Probably not perfect (maybe $scheme://$host$uri instead?), but it’ll work.

    One more modification. Most of us know about PHP upload limits, but nginx has its own upload limits as well:

    http://wiki.nginx.org/NginxHttpCoreModule#client_max_body_size

    If you attempt to upload a file that is too large, WordPress displays “HTTP error.” Which is super descriptive. I recommend setting the nginx ‘client_max_body_size’ to something a bit larger than PHP’s ‘post_max_size’ setting, which should be larger than PHP’s ‘upload_max_filesize’ setting. Can be in the ‘http’ block or the ‘server’ block. Depends on your preference where you want it. I just stuffed it into the ‘http’ block.

    http {
    	client_max_body_size 13m;
    ...
    }

    @bigsite Great and finally get FPM+APC+nginx on my vps. but now there’re some minor problems:

    1. I’m using WP Super Cache and permalink structure /%post_id%/ for my blogs. as logged in users who access domian/blog/post_id will be redirected to domian/blog/post_id/, that’s what we expected. right? but as visitors no redirects applied to urls without trailing slash. I think maybe somehing wrong with wp-super-cache rewrite?

    2. How to bypass requests for uploaded files to php-fpm by runing that php script? Could you please tell me a little more?

    Big thanks to your this post. It saves my time, and money too.

    FYI. This post have an easy way to setup nginx + php-fpm + apc, but it’s centos only: http://rob.olmos.name/2010/08/centos-5-5-php-5-3-3-php-fpm-nginx-rpms

    @sparanoid – For your first question, it might be your permalink structure choice. For WP Super Cache to work properly, it prefers a “fancy permalink structure”, which I’m not quite sure what that means. I’m not sure how a permalink structure of /%post_id%/ fits into that picture. Using the supplied rules, as a visitor, the URLs without trailing slashes redirects to the correct location for me, which means the visitor is probably hitting WP. To get past that redirect issue using nginx rules, I would change the WP Super Cache rules slightly:

    set $supercacheuri "";
    set $supercachefile "$document_root/wp-content/cache/supercache/${http_host}${uri}/index.html";
    if (-e $supercachefile) {
    	set $supercacheuri "/wp-content/cache/supercache/${http_host}${uri}/index.html";
    }
    
    ...
    
    # Stop processing if the supercache file is valid.
    if ($supercacheuri) {
    	rewrite [^/]$ $scheme://$host$uri/ permanent;
    	rewrite ^ $supercacheuri break;
    }

    That should detect that there is a cache file available with a trailing slash, then detect that the current $uri doesn’t have a trailing slash, and execute a redirect to the correct URL. These changes are untested but should work.

    As to the second question, that is experimental, custom PHP code that requires tweaking for your specific setup. In theory, you should be able to create a new subdirectory on your server inside your WP install, upload a .php file with the code provided above (modified to point at the correct paths), and then run the PHP file using a web browser. It will create symbolic links in the correct place for each blog. The nginx rules will automatically pick this up and start serving images without passing the request to PHP. IMO, you should ask Donncha to add similar functionality to WP Super Cache, because it really makes more sense to be part of a caching plugin and not in some hacky solution. The code provided is just an example.

    @bigsite – The new rewrite rule works great, now correct trailing slash added to all visitors. Thank you!

    The second problem, I got the point, I created a wp-ms-symbolic.php in root directory, and then run it with crontab (I think it’s better run it as a plugin and create a hook when new site are created). but it only created symblic link to the latest added site? so I have to manually added old sites by using the following command:

    # Go to wp-content/cache/ms-filemap/domain.tld/ first
    ln -s /srv/www/domain.tld/public_html/wp-content/blogs.dir/BLOG_ID/ username

    Then uploaded files bypass the php and follow location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ corrently. cheers.

    So wp-super-cache need a rewrite rule to be added? I’m confused because so far i don’t need to add rewrite rule for wp-super-cache in nginx

    @serversreview – These rules are intended for WP Super Cache in ‘mod_rewrite’ mode. If you aren’t running in this mode, you are invoking PHP every page load under nginx. You can certainly run WP Super Cache that way, but it isn’t as efficient. Unless, of course, I misunderstood what you mean.

    Hi, bigsite, I found a serious problem, when I try to delete cache from WP Super Cache, All files uploaded to ./wp-content/blogs.dir will be deleted.

    @sparanoid – That might be your symbolic links kicking in. Don’t do rm -rf in the /cache subdirectory or let any scripts do the same.

    If you are concerned about this, maybe move the symlinks to another directory instead of the /cache directory. Also, of course, keep regular backups.

Viewing 15 replies - 1 through 15 (of 58 total)
  • The topic ‘[Resolved] nginx php-fpm PHP APC WordPress multisite (subdirectory) WP Super Cache’ is closed to new replies.