
I ran a simple test against Nginx v0.5.22 and Apache v2.2.8 using ab (Apache’s benchmarking tool). During the tests, I monitored the system with vmstat and top. The results indicate that Nginx outperforms Apache when serving static content. Both servers performed best with a concurrency of 100. Apache used four worker processes (threaded mode), 30% CPU and 17MB of memory to serve 6,500 requests per second. Nginx used one worker, 15% CPU and 1MB of memory to serve 11,500 requests per second.
–The Linix Journal
This article describes how to set up Nginx as a reverse proxy static cache for WordPress. This web server stack will enable you to scale single or multiple WordPress installations on a medium to low end VPS or dedicated server. It is also the same stack this website runs on along with about 10 other WordPress sites on an unmanaged Xen VPS with only 768M of Ram.
WordPress Performance Plugin Stack
This setup requires Dan Collis-Puro’s Nginx proxy cache integrator plugin and works great with W3 Total Cache
Our W3 Total Cache setup is page caching with disk enhanced, minify with disk and APC as an object and database cache. By setting page cache and minify to disk they will be served by Nginx.
We are also utilizing W3T’s great self hosted CDN feature and have another domain on this same server with 4 cnames pointing to it.
Browsers can only download a few files at once, only 4 in some cases. Pipelining is a technique whereby aliases (subdomains for example) of your server are used to allow your browser to increase the practical limit of files that can be downloaded in parallel. Doing so maximizes the throughput of the your internet connection and allows the browser to render a page faster. W3TC takes care of managing these files transparently once DNS CNAMEs (aliases) and subdomains are properly configured.
–Frederick Townes, Author W3 Total Cache
How to configure Nginx to serve static files and pass the heavy PHP and WordPress lifting to Apache
The problem with using Apache alone is that it opens up a connection and hits php on every request even for static files. This wastes connections because Apache will keep them open and when you have lots of traffic your connections will be bogged down even if they are not being used.
By default Apache listens for requests on port 80 which is the default web port. First we are going to make changes to our Apache conf and virtual hosts files to listen on port 8080.
Apache Config
httpd.conf
set KeepAlive to off
ports.conf
NameVirtualHost *:8080 Listen 8080
Per Site Virtual Host
<VirtualHost 127.0.0.1:8080>
ServerAdmin info@yoursite.com
ServerName yoursite.com
ServerAlias www.yoursite.com
DocumentRoot /srv/www/yoursite.com/public_html/
ErrorLog /srv/www/yoursite.com/logs/error.log
CustomLog /srv/www/yoursite.com/logs/access.log combined
</VirtualHost>
You should also install mod_rpaf so your logs will contain the real ip addresses of your visitors. If not your logs will have 127.0.0.1 as the originating ip address.
Nginx Config
On Debian you can use the repositories to install but they only contain version 0.6.33. To install a later version you have to add the lenny backports packages
$ nano /etc/apt/sources.list
Add this line to the file deb http://www.backports.org/debian lenny-backports main
$ nano /etc/apt/preferences
Add the following to the file:
Package: nginx Pin: release a=lenny-backports Pin-Priority: 999
Issue the following commands to import the key from backports.org to verify packages and update your system’s package database:
wget -O - http://backports.org/debian/archive.key | apt-key add -
$ apt-get update
Now install with apt-get
apt-get install nginx
This is much easier than compiling from source.
Nginx conf and server files config
nginx.conf
user www-data;
worker_processes 4;
error_log /var/log/nginx/error.log;
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;
client_body_temp_path /var/lib/nginx/body 1 2;
gzip_buffers 32 8k;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
tcp_nodelay on;
gzip on;
gzip_comp_level 6;
gzip_http_version 1.0;
gzip_min_length 0;
gzip_types text/html text/css image/x-icon
application/x-javascript application/javascript text/javascript application/atom+xml application/xml ;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Now you will need to set up your Nginx virtual hosting. I like to use the sites-enabled method with each v host sym linked to a file in the sites-available directory.
mkdir /etc/nginx/sites-available
$ mkdir /etc/nginx/sites-enabled
$ touch /etc/nginx/sites-available/yourservername.conf
$ touch /etc/nginx/sites-available/default.conf
$ ln -s /etc/nginx/sites-available /etc/nginx/sites-enabled
$ nano /etc/nginx/sites-enabled/default.conf
default.conf
Note:
The static cache settings in the following files will only work if the Nginx proxy cache integrator plugin is enabled.
proxy_cache_path /var/lib/nginx/cache levels=1:2 keys_zone=staticfilecache:180m max_size=500m;
proxy_temp_path /var/lib/nginx/proxy;
proxy_connect_timeout 30;
proxy_read_timeout 120;
proxy_send_timeout 120;
#IMPORTANT - this sets the basic cache key that's used in the static file cache.
proxy_cache_key "$scheme://$host$request_uri";
upstream wordpressapache {
#The upstream apache server. You can have many of these and weight them accordingly,
#allowing nginx to function as a caching load balancer
server 127.0.0.1:8080 weight=1 fail_timeout=120s;
}
Per WordPress site conf (For multi site you will only need one vhost)
server {
#Only cache 200 responses, and for a default of 20 minutes.
proxy_cache_valid 200 20m;
#Listen to your public IP
listen 80;
#Probably not needed, as the proxy will pass back the host in "proxy_set_header"
server_name www.yoursite.com yoursite.com;
access_log /var/log/nginx/yoursite.proxied.log;
# "combined" matches apache's concept of "combined". Neat.
access_log /var/log/apache2/nginx-access.log combined;
# Set the real IP.
proxy_set_header X-Real-IP $remote_addr;
# Set the hostname
proxy_set_header Host $host;
#Set the forwarded-for header.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
# If logged in, don't cache.
if ($http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
set $do_not_cache 1;
}
proxy_cache_key "$scheme://$host$request_uri $do_not_cache";
proxy_cache staticfilecache;
proxy_pass http://wordpressapache;
}
location ~* wp\-.*\.php|wp\-admin {
# Don't static file cache admin-looking things.
proxy_pass http://wordpressapache;
}
location ~* \.(jpg|png|gif|jpeg|css|js|mp3|wav|swf|mov|doc|pdf|xls|ppt|docx|pptx|xlsx)$ {
# Cache static-looking files for 120 minutes, setting a 10 day expiry time in the HTTP header,
# whether logged in or not (may be too heavy-handed).
proxy_cache_valid 200 120m;
expires 864000;
proxy_pass http://wordpressapache;
proxy_cache staticfilecache;
}
location ~* \/[^\/]+\/(feed|\.xml)\/? {
# Cache RSS looking feeds for 45 minutes unless logged in.
if ($http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
set $do_not_cache 1;
}
proxy_cache_key "$scheme://$host$request_uri $do_not_cache";
proxy_cache_valid 200 45m;
proxy_cache staticfilecache;
proxy_pass http://wordpressapache;
}
location = /50x.html {
root /var/www/nginx-default;
}
# No access to .htaccess files.
location ~ /\.ht {
deny all;
}
}
Self Hosted CDN conf
For your self hosted CDN conf you only need to set it up to serve static files without the proxy pass
server {
proxy_cache_valid 200 20m;
listen 80;
server_name yourcdndomain.com;
access_log /srv/www/yourcdndomain.com/logs/access.log;
root /srv/www/yourcdndomain.com/public_html/;
proxy_set_header X-Real-IP $remote_addr;
location ~* \.(jpg|png|gif|jpeg|css|js|mp3|wav|swf|mov|doc|pdf|xls|ppt|docx|pptx|xlsx)$ {
# Cache static-looking files for 120 minutes, setting a 10 day expiry time in the HTTP header,
# whether logged in or not (may be too heavy-handed).
proxy_cache_valid 200 120m;
expires 7776000;
proxy_cache staticfilecache;
}
location = /50x.html {
root /var/www/nginx-default;
}
# No access to .htaccess files.
location ~ /\.ht {
deny all;
}
}
Now start the servers
/etc/init.d/apache2 restart
$/etc/init.d/nginx start
The Benchmark Results
To test this setup and demonstrate the potential I ran Apache Bench on the home page of this site and set it to make 5000 requests at a concurrency level of 100. The results:
Requests per second: 8242.83 [#/sec] (mean)
While running the benchmark I had htop open in another window and as you can see by the screen shot below, Nginx only opened 3 worker processes and barely made a blip on memory or cpu.

ab -n 5000 -c 100 http://wp-performance.com/
[root@xxxxxxxxx ~]#ab -n 5000 -c 100 http://wp-performance.com/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking wp-performance.com (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: nginx/0.7.65
Server Hostname: wp-performance.com
Server Port: 80
Document Path: /
Document Length: 9348 bytes
Concurrency Level: 100
Time taken for tests: 0.607 seconds
Complete requests: 5000
Failed requests: 0
Write errors: 0
Total transferred: 49172298 bytes
HTML transferred: 46870872 bytes
Requests per second: 8242.83 [#/sec] (mean)
Time per request: 12.132 [ms] (mean)
Time per request: 0.121 [ms] (mean, across all concurrent requests)
Transfer rate: 79163.82 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 0.9 1 9
Processing: 3 11 1.8 11 61
Waiting: 1 5 2.1 5 54
Total: 5 12 1.7 12 61
Percentage of the requests served within a certain time (ms)
50% 12
66% 12
75% 13
80% 13
90% 14
95% 15
98% 15
99% 16
100% 61 (longest request)

hello and thanks for this great guide. I really could use a bit of help here. I am trying to set up nginx as front end proxy to my apache but I have a couple of sites on my server so I need a way to handle all apache2 vhosts automatically.
I am trying to follow this tutorial and it almost works: http://forum.nginx.org/read.php?11,146847 The basic problem I run into is that wordpress with multisite enabled as sub-domains doesn’t work with that setup and nginx wasn’t able to resolve my servername so I had to make him listen on a single IP…
I am even willing to pay for help. I have been googleing for days on end and can’t figure this one out on my own but a few hitns and tips would be great too as I am quite happy to test
Hi Ovidiu,
Can you tell me what you have in your Apache v host files and also the contents of your nginx.conf and your nginx server files?
If you create a Pastbin for them I will take a look for you.
basically I am running a server for my and friends sites (non-commercially) and do so using the ISPCFG3 free control panel. This means I need a seamless nginx proxy implementation as fiddling around with other apache2 settings than the ports would break the ISP panel. All apache2 vhosts are either running suPhp or fastCGi I just want to take the static part off apache and have nginx serve it. This tutorial: http://www.howtoforge.com/nginx-catch-all-host-as-front-end-to-apache-for-ispconfig-3-on-debian-lenny sounded like what I needed but it has a few problems in it. Meanwhile I solved almost all of them, have now 2 vhost files for nginx, 1 that is a catchall and handles all vhosts except for one which runs wordpress with multiuser enabled and needs wildcard domain. I fixed that too but now I am doubting that it makes sense.
here are my two vhost files for nginx: http://pastebin.com/B0NHjxBx
I had to copy these rewrites from here for the wildcard subdomain: http://codex.wordpress.org/Installing_WPMU#Rewriting_Rules_for_others_HTTP_Server btw they still mention blogs.php I changed it to reflect ms-sites.php and all is well.
Now what makes me frown and ask myself if I am wasting my time here is that actually all static content that should have been handled by nginx seems to fall back to apache2 because of this rewrite. am I right?
so basically nginx works as a front end proxy for all vhosts except this one which actually is the biggest one on the server? have I just wasted 3 days getting this to work?
wait. don’t bother. been up for 24h but I think its working. going to bed. will report back tomorrow!
quick question: in your nginx config above I don’t see you declare any root except for the 50x and the CDn… where does nginx serve the static files from?
It’s actually not needed. I don’t fully understand the concept but it has something to do with the proxy set header and mod_rpaf.
The nginx server files listed here are slightly modified from the the instructions in the Nginx proxy cache integrator plugin.
The location is set in the cdn conf because it would not work without it. All the sites with the upstream WordPress installation work with the document root only being set in the Apache Vhost.
ok, I changed my config too and it works. do you happen to understand why the example of the mentioned plugin has a `proxy_pass` directive in the static content location? Doesn’t make any sense to me as we want nginx to handle it, right? I also see you don’t have one in there..
I’m not sure but the static files are forwarded using the proxy_cache directive which sends static requests to the staticfilecache. I’m am going to do some testing with and without the proxy_pass directive in the static files locations and see what happens.
We’d love your feedback on our new wordpress monitoring and management tool – see inside your app and see what’s slow, 2 minutes after installing our agent. If you have WordPress performance problems, you should try out New Relic — http://www.newrelic.com – we just released an agent that let’s you see inside your wordpress/php app and see what’s slow. We’d love your feedback. Check out how we do it: http://blog.newrelic.com/2010/12/16/measuring-wordpress-performance-with-new-relic-rpm/
It is super easy to setup. Let me know if you think this might be valuable!
Hi thanks for this tutorial.
I’ve setup something very similar to this tutorial, but when I run apache bench tests I get nothing close to your # Req / sec.
What kind of hardware are you using for your website?
Was this done on a lan or accross the Internet?
Here are my results:
Hey Chris,
Great tutorial – really useful for our current nginx proxy setup tests.
We did hit a snag with the apache hosts setup which I thought I’d share here in case anyone else comes across it.
We’ve 2 sites running under apache and for the “Per Site Virtual Host” section we had to change the line:
to
as apache didn’t like declaring multiple sites with an explicit IP address. There may be another way around this via host files or something like that but Apache docs recommend not declaring a specific IP for multiple virtual hosts when there is only 1 IP available – see here:
http://httpd.apache.org/docs/2.0/vhosts/examples.html
Once we solved that snag, we were good to go!
Ed
Glad you got it working. I’m about to write another post updating the changes I’ve made since upgrading to Nginx 1.0 and moving my whole setup to a Multisite installation.
Hi Chris,
this guide looks really, really interesting. I’m now tempted to try it out myself.
Is there any good installation guide for nginx on CentOS that you would recommend to nginx beginners?
Will nginx also serve downloadable files such as .zip files and can I still use mod_rewrite when I use nginx to serve files? Too many questions <.<
Thanks and keep up the interesting blog, this one will be bookmarked for sure!
Oliver
I just did an Nginx install on Cent OS running cPanel. It’s not very fun and would be easier to scrape CentOs for Debian.
I would like to post two links that I found very helpful.
1. This is a plugin that is supposed to make WordPress permalinks compatible with nginx
http://wordpress.org/extend/plugins/nginx-compatibility/
2. Here is a pretty long guide about nginx
https://calomel.org/nginx.html
I will keep you posted when I find any other useful guides and plugins. The problem I currently have with nginx => no apache modules. The most mission critical apache module is mod_security and without that you are prone to any attacks.
Check out my latest article: WordPress Performance Server – Debian “squeeze” with Nginx, APC and PHP from the Dotdeb repos