Archive for the ‘Nginx’ category

Securing Nginx and PHP

December 16th, 2011

Disclaimer
This write up is intended for single-user systems where you are the only user expected to log in via shell/terminal/sftp (or at least people you actually trust). This collection of tips does not cover situations where you may have multiple users home folders or a shared hosting situation utilizing nginx and php-fpm. Generally speaking if you have to read this post for security tips you probably should not be allowing access to any other user but yourself in the first place.

If you do not care to read this whole write up, just remember one thing: `777` is not a magical quick-fix; it’s an open invitation to having your system compromised. (Script-Kiddies… can you say jackpot?)

User/Groups

Generally speaking your server, which will most likely be a VPS running some fashion of linux will already have a web service user and group. This will sometimes be www, www-data, http, apache, (or even nginx if a package manager installed it for you). You can run the following command to get a list of users that are on your system.

cat /etc/passwd

Both Nginx and PHP-FPM should run as a web service, on a Debian squeeze this would be www-data:www-data, or on FreeBSD www:www.

If your server was set up with root being the main user you should create an unprivileged user for handling your web files. This will also make it easier to handle permissions when uploading your web files via SFTP. For example the following command on a debian system would create a user named kbeezie which has www-data as the primary group.

useradd -g 33 -m kbeezie

Group ID #33 is the id for www-data on Debian Squeeze (you can verify with id www-data). You may have to su into the new user and change the password (or usermod to change). This will also create a home folder in /home/kbeezie/ by default. You can log in via SFTP to this user and create a www folder if you wish. You’ll notice that the files will be owned by kbeezie:www-data, which will allow Nginx and PHP to read from, but also gives you group-level control over how the web services may treat those files.

This is ideal since you’re not giving nginx or php-fpm too much control over the user’s files and they can be controlled with the group flag. You could also create the user with it’s own group such as kbeezie:kbeezie and just change the group of the web files to www-data where appropriate.

SSH Options

It is usually advisable to disable Root access via /etc/ssh/sshd_config with the following line:

PermitRootLogin no

However make sure you can log in with your new unprivileged user via SSH, and also make sure that you can `su` into root permission. On a FreeBSD system only a user belonging to the wheel group can su into root, and only a user listed in the sudoers file can use the sudo command. However on Debian/etc the user could have www-data as it’s own group and still be able to su/sudo as long as the root password is valid. Your password should be at least 12 characters long and contain digits, symbols and at least one capital letter. Do not use the same password for root and your web user.

Once you’ve verified that you’re able to obtain root status with the new user you can safely disable root log in via sshd_config and restart the ssh deaemon.

You should also change your default SSH port, which is 22. While a port scanner could probably find the new SSH port it is usually best practice not to use the default port for any type of secure access. Like before make sure you can log into the new port (you can configure sshd_config to listen to both 22 and another port to test this out).

SSH – PubKey Authentication

If you are on OSX or another unix/linux operating system, like I am, setting up pub key authentication is fairly painless. Logged in as your web user on the server you can run the following command:

ssh-keygen

The above by default will ask for a passphrase for your private key as well as a destination to save both the id_rsa and id_rsa.pub files (which will normally be ~/.ssh/). You can then copy your own user’s public key to an authorized_key file with the following command.

cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys

Then on your own computer you can run the ssh-keygen command as well, copy your own computer’s public key from the id_rsa.pub file and add it as another line to your server’s authorized_keys file.

If you have `PubkeyAuthentication yes` listed in your sshd_config file with the authorized key path being that of .ssh in your home folder the server should allow you to log in without prompting you for a password. Just remember that if you chose not to use a passphrase for your private key then it is possible for anyone who obtains your id_rsa* files to log into your server without being prompted for a password.

You can even turn off password authentication completely and rely solely on public key authentication by setting `PasswordAuthentication no` in your sshd_config file. However keep in mind, unless you have another means of getting into your server you might get locked out if you lose your public/private key or access to the machine you use to log in (also not every SFTP or Terminal application supports pub key authentication).

I actually have public key authentication set up with my iPad using iSSH for quick server access on the go (and you do not need a jailbroken iPad for this).

On the next page are some Nginx and PHP specific configurations to hardening your installation.

Nginx Flood Protection with Limit_req

April 9th, 2011

The Test

I’ll show you a very simple demonstration of Nginx’s Limit Request module and how it may be helpful to you in keeping your website up if you are hit by excessive connections or HTTP based denial-of-service attacks.

For this test I’m using a copy of my site’s about page saved as about.html, and the Blitz.io service (which is free at the moment) to test the limit_req directive.

First I test the page with the following command in Blitz, which will essentially ramp the number of concurrent connections from 1 to 1,075 over a period of 1 minute. The timeout has been set to 2 minutes, and the region set to California. I also set it to consider any response code other than 200 to be an error, otherwise even a 503 response will be considered a hit.

-p 1-1075:60 --status 200 -T 2000 -r california http://kbeezie.com/about.html

Not bad right? But what if that were a php document. That many users frequently might cause your server to show 502/504 errors as the php processes behind it keep crashing or timing out. This is especially true if you’re using a VPS or an inexpensive dedicated server without any additional protection. (Though if you use either, iptables or pf might be a good resource to look into)

You can of course use caching and other tools to help improve your website, such as using a wordpress caching plugin, which you should be using anyways. But sometimes that one lone person might decide to hit one of your php scripts directly. For those type of people we can use the limit request module.

Let’s create a zone in our http { } block in Nginx, we’ll call it blitz and set a rate of 5 request per second, and the zone to hold up to 10MB of data. I’ve used the $binary_remote_addr as the session variable since you can pack a lot more of those into 10MB of space than the human readible $remote_addr.

limit_req_zone $binary_remote_addr zone=blitz:10m rate=5r/s;

Then inside my server block I set a limit_req for the file I was testing above:

location = /about.html {
	limit_req zone=blitz nodelay;
}

So I reload Nginx’s configuration and I give Blitz another try:

You’ll notice that now only about 285 hits made it to the server, thats roughly 4.75 requests per second, just shy of the 5r/sec we set for the limit. The rest of the requests were hit with a 503 error. If you check out the access log for this you’ll see that a majority of the requests will be the 503 response with the spurts of 200 responses mixed in there.

Using this can be quite helpful if you want to limit the request to certain weak locations on your website. It can also be applied to all php requests.

Applying Limit Request to PHP

If you would like to limit all access to PHP requests you can do so by inserting the limit_req directive into your php block like so:

location ~ \.php {
	limit_req   zone=flood;
	include php_params.conf;
	fastcgi_pass unix:/tmp/php5-fpm.sock;
}

It may help to play with some settings like increasing or decreasing the rate, as well as using the burst or nodelay options. Those configuration options are detailed here: HttpLimitReqModule.

Note about Blitz.io

You may have noticed from the graphs above that the test were performed for up to 1,075 users. That is a bit misleading. All of the hits from the California region came from a single IP (50.18.0.223).

This is hardly realistic compared to an actual rush of traffic, or a DDOS (Distributed Denial of Service) attack. This of course also explains why the hits are consistent with the limiting of a single user as opposed to a higher number that reflecting a higher number of actual users or IP sources. Load Impact actually uses separate IPs for their testing regions. However with the free version you are limited to a max of 50 users and number of times you may perform the test. You would have to spend about $49/day to perform a test consisting up to 1,000 users to your site.

Testing from a single IP can be easily done from your own server or personal computer if you got enough ram and bandwidth. Such tools to do this from your own machine include: siege, ab, openload and so forth. You just don’t get all the pretty graphs or various locations to test from.

Also if you were testing this yourself, you have to remember to use the –status flag as Blitz will consider 5xx responses as a successful hit.

Better Alternatives

I won’t go into too much details, but if you are serious about protecting your site from the likes of an actual DDOS or multi-service attack it would be best to look into other tools such as iptables (linux), pf (packet filter for BSD) on the software side, or a hardware firewall if your host provides one. The limit request module above will only work for floods against your site over the HTTP protocol, it will not protect you from ping floods or various other exploits. For those it helps to turn off any unnecessary services and to avoid any services listening on a public port that you do not need.

For example on my server, the only public ports being listened on are HTTP/HTTPS and SSH. Services such as MySQL should be bound to a local connection. It may also help to use alternate ports for common services such as SSH, but keep in mind it doesn’t take much for a port sniffer to find (thats where iptables/pf come in handy).

Search Page Getting Hammered?

March 27th, 2011

On my WordPress Caching write up someone mentioned asked a very good question in the comments. What good is the caching if your site gets brought down by excessive search queries?

Great article.

However I have following problem.

My WP setup is like that.

Currently I am on shared hosting with WP + W3 Total cache and during peak hours, my site is very slow. That is mainly because I have a huge traffic from Google.

My webstie caches plenty of keywords with AskApache Google 404 and Redirection.

What happens is that traffic from Google goes to /search/what-ever-keywords dynamicly created everytime. And that is killing my system.
The problem is I have no idea how to help poor server and cache that kind of traffic.

Would you have any advice for that ?
Regards,
Peter

Fortunately the Nginx webserver has a way to soften the impact with a module called Limit Request. For wordpress this is how you would implement it:

http {
	limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
	#... the rest of your content
 
	server {
		#... your server content
 
		location /search { 
			limit_req zone=one burst=3 nodelay;
			try_files $uri /index.php; 
		}
	}
}

What we have here is almost identical to the example provided in the Nginx Wiki (HttpLimitReqModule). Essentially we’ve created a zone that allows a user no more than 1 requests per seconds, assigning them via $binary_remote_addr (which is smaller than $remote_addr while still accomplishing the same goal), within a space of 10MB.

Then we’ve placed a limit_req directive into the /search location. (Remember to include the rewrite so that index.php receives the search request in wordpress) This directive uses the zone we created earlier, allowing for a burst of up to 3 requests from a single user. If the user exceeds the limit too many times (the burst limit) they’re presented with a 503 error which Google and others consider sort of a ‘back off’ response.

By default the burst value is zero.

We use try_files here because rewriting will occur before the limiting had a chance to act, since the rewrite phase takes precedence before most other processes.

Here is an example wordpress configuration utilizing the above, configured for W3TC w/ Memcache (or OpCode Caching), please see the article [ The Importance of Caching WordPress] for details on caching WordPress in other ways, and why you should be using caching with wordpress.

This is config is somewhat based off the same configuration I currently use for kbeezie.com on a FreeBSD server.

 
user kb244 www;
worker_processes  2;
 
events {
	worker_connections	2048;
}
 
 
http {
	sendfile on;
	tcp_nopush on; # Generally on for linux, off for FreeBSD/Unix
	tcp_nodelay on;
	server_tokens off;
	include mime.types;
	default_type  application/octet-stream;
	index index.php index.htm index.html redirect.php;
 
	#Gzip
	gzip  on;
	gzip_vary on;
	gzip_proxied any;	
	gzip_comp_level 6;
	gzip_buffers 16 8k;
	gzip_http_version 1.1;
	gzip_disable "MSIE [1-6].(?!.*SV1)";	
	gzip_types text/plain text/css application/json application/x-javascript text/xml 
                        application/xml application/xml+rss text/javascript;
 
	#FastCGI
	fastcgi_intercept_errors on;
	fastcgi_ignore_client_abort on;
	fastcgi_buffers 8 16k;
	fastcgi_buffer_size 32k;
	fastcgi_read_timeout 120;
	fastcgi_index  index.php;
 
	limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
 
	server {
		listen 80;
 
		# Replace with your domain(s)
		server_name kbeezie.com www.kbeezie.com;
 
		# Setting your root at the server level helps a lot
		# especially if you don't like having to customize your
		# PHP configurations every single time you create a vhost
		root /usr/local/www/kbeezie.com;
 
		# Always a good idea to log your vhosts seperately
		access_log /var/log/nginx/kbeezie.access.log;
		error_log /var/log/nginx/kbeezie.error.log;
 
		# I'm using W3TC with memcache, otherwise this block
		# would look a lot more complicated for handling file
		# based caching.
		location / { 
			try_files $uri $uri/ /index.php; 
		}
 
		# And here we have the search block limiting the requests
		location /search {
			limit_req zone=kbeezieone burst=3 nodelay;
			try_files $uri /index.php;
		}
 
		# We want Wordpress to handle the error page
		fastcgi_intercept_errors off;
 
		# Handle static file requests here setting appropiate headers
		location ~* \.(ico|css|js|gif|jpe?g|png)$ {
			expires max;
			add_header Pragma public;
			add_header Cache-Control "public, must-revalidate, proxy-revalidate";
		}
 
		# I prefer to save this location block in it's own php.conf
		# in the same folder as nginx.conf so I can just use:
		# include php.conf;
		# at the bottom of each server block I want PHP enabled on
 
		location ~ \.php$ {
			fastcgi_param  QUERY_STRING       $query_string;
			fastcgi_param  REQUEST_METHOD     $request_method;
			fastcgi_param  CONTENT_TYPE       $content_type;
			fastcgi_param  CONTENT_LENGTH     $content_length;
 
			fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
			fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
			fastcgi_param  REQUEST_URI        $request_uri;
			fastcgi_param  DOCUMENT_URI       $document_uri;
			fastcgi_param  DOCUMENT_ROOT      $document_root;
			fastcgi_param  SERVER_PROTOCOL    $server_protocol;
 
			fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
			fastcgi_param  SERVER_SOFTWARE    nginx;
 
			fastcgi_param  REMOTE_ADDR        $remote_addr;
			fastcgi_param  REMOTE_PORT        $remote_port;
			fastcgi_param  SERVER_ADDR        $server_addr;
			fastcgi_param  SERVER_PORT        $server_port;
			fastcgi_param  SERVER_NAME        $server_name;
 
			fastcgi_pass unix:/tmp/php5-fpm.sock;
 
			# I use a Unix socket for PHP-Fpm, yours instead may be:
			# fastcgi_pass 127.0.0.1:9000;
		}
 
		# Browsers always look for a favicon, I rather not flood my logs
		location = /favicon.ico { access_log off; log_not_found off; }	
 
		# Make sure you hide your .hidden linux/unix files
		location ~ /\. { deny  all; access_log off; log_not_found off; }
	}
}

As always a little research and tweaking to your own requirements is advised.

Kwolla; shows promise but lacks professionalism.

March 16th, 2011

Over the years I’ve seen several social network scripts and applications pop up, dolphin, socialengine, buddypress and so forth. I’ve worked with SocialEngine on two major occasions (2.8 and 4.1.2), and still determined it to be a very temperamental piece of software that suffers far worse under the hood than an uncached copy of wordpress. I’ve already went thru the painstaking process of making a script like SocialeEngine work rather decently with a fast Nginx server configuration. So naturally any time I see a social networking script pop up that wasn’t specifically built for a website, I consider it bloatware.

I originally learned of Kwolla earlier this month from a friend noticing it on reddit. As of March 4th 2011 according to his development blog was made free as part of a promotional effort. Kwolla originally came out in January 2011 for a price of 149 USD, and was dropped to 49 USD until it was eventually made free less than a month later.

Product Website

I first visited Kwolla.com roughly around the 5th, shortly after the free promotional announcement. The website was clean, somewhat easy to understand but lacked specific information I was seeking, namely server requirements.

The main site has plenty of features listed, themes, modules, video capability, translations and so forth. However there is no wiki, forum, and the knowledge base is empty. No mentions on the website on what server, php, mysql versions are required. However there is a “free” compatibility download to test your hosting.

Customer support area at the time was a contact form. It is now something of a discussion panel, with no current discussions and an empty knowledge based hosted on TenderApp.

Because there’s no technical specification listed on the main website, you either have to click on a host that Kwolla works with (half of which are affiliate links… can you blame him?) or you have to download the Kwolla Compatibility check.

Checking Compatibility

When you download the kwolla-compatibility package which is available as a “Free Download”, and unzip it, you’ll notice approximately 21 files, with only one that really matters (the rest are graphics such as suggested host logos).

Now if you don’t have MySQLi extension for PHP installed… you’ll likely never know as the compatibility crashes to a blank page during the check due to lack of the MySQLi extension. While it checks for the existance of various functions, it just uses MySQLi functionality blindly which of course won’t be handled properly if you don’t have that extension installed.

I noticed that it said my Apache version was of the ‘right version’. Problem is I don’t use the Apache webserver at all, and it checks by means of the apache_get_version() function. If the function doesn’t exist then the compatibility check considers it valid. Likewise every other apache module passed as well since it uses apache_get_modules() function to test for the availability of such module. Like before, if the functions don’t exist, it considers the tests passed. However most potential customers are going to be using Apache, and will likely see a more realistic compatibility report.

Installation

When I originally tried Kwolla it was version 1.1.0, which based on the installation guide (another “free download”) was probably written with the idea that another developer would be installing it. There was command line instructions for setting up a MySQL database, and importing the provided kwolla.sql file. This was later changed to screenshots of Cpanel with phpMyAdmin for Kwolla 1.1.1. (Should note, due to using MySQL 5.5, I had to change every instance of TYPE= to ENGINE= in the sql file in order to get it to import correctly)

Originally you had to upload the 1.1.0 framework, and set the virtualhost to point to the httpdocs folder while the rest of the application logic was above the web-accessible location. In 1.1.1 this was changed to where the upload folder was the root of the application, with a sole index.php in the root. This of course would be friendlier to your typical end users. I noticed the user guide for 1.1.1 was changed to state that other webservers were untested after my initial query with the developer regarding Nginx, and that some requirements were updated in the install guide, but still missing on the main website.

The installation guide also instructs you to assign all files to a permission of 755, particularly the shell scripts provided for cron jobs. However doing so is incredibly insecure especially when you consider social networking allows for uploads of files that could potentially be processed by the server.

Needless to say installation wasn’t too bad for me, since I’m used to working with the command line interface, and handling sql dumps as well as configuring virtual hosts from a text file. My only real obstacle was converting the .htaccess to something that would work within Nginx, as such this is the converted configuration I came up with:

server {
       access_log off;
       listen 80;
       error_log /var/log/nginx/kwolla.error.log;
 
       server_name community.domain.com;
       root /usr/local/www/community.domain.com/;
 
       location ~ ^.*.svn/* {
               rewrite ^ / last;
       }
 
       location / {
               try_files $uri $uri/ @rewrites;
       }
 
       location @rewrites {
               rewrite ^/profile/(.*)/(status|friends|comments) /index.php?u=profile/$2/$1&$query_string last;
               rewrite ^/profile/(.*) /index.php?u=profile/view/$1&$query_string last;
               rewrite ^ /index.php?u=$request_uri? last;
       }
 
       include drop.conf;
       include php.conf;
}

It is far from perfect, but it has been working thus far. Except in a few instances where links were hard coded as relative paths, or direct to index.php such as placing comments on other members profiles.

Installed

Initially I had just a blank screen, since I had left out the mysqli extension on this particular jailed installation, and support didn’t help too much other than stating that there was a secondary log file for debugging. But also making emphasis that only apache was supported at this time (though will be providing compatibility with nginx, lighttpd and others soon). Once I was able to identify the error as a missing mysqli extension I was able to move on with the installation.

Once the script was installed (correctly), I was presented with an empty community. As the installation guide suggests you should go ahead and create your profile first. So in my line of thinking, I was assuming that either the site is controlled by the primary participant, or controlled by whatever member shares the same email address as the owner in the configuration file.

Needless to say there really is no control other than for your own profile. On version 1.1.1 every member that signs up appears to have the same ability as yourself. While functions like comments, sending private message, uploading photographs or videos appeared to work fine, you essentially had no ‘command central’ so to speak.

So I shot off an email with a few questions and suggestions, namely:

  • MySQL 5.5 note
  • Nginx configuration (0.8.x-0.9.x)
  • The Curious Missing Admin panel

On march 6th I received the following response;

Hi Karl,
 
The install script will work with MySQL 5.1, it's what I'm running.
You're right, TYPE has been replaced with ENGINE, but 99% of all users
will be using MySQL 5.0 or 5.1 because it's what comes with a shared
hosting account or whats by default in most major Linux distributions.
 
Thank you for the nginx information and configuration.
 
>> Though I'm a tad confused... where's the administrator panel, 
>> or area to set member's permissions on the site?
 
There isn't one. It will be available in version 2.0.

Not exactly pleasant about the lack of an administrative control panel. Since its one feature I would consider especially important for any kind of ‘social networking’, especially if you have spam accounts, or disruptive members. What is an end user to do but figure out how to remove or change such a profile via the MySQL database and files? For something that was once sold for $149, the lack of administrative control seemed like a deal breaker to me.

Kwolla 2.0 Pre-Sale

Earlier yesterday I received a promotional email that the next version 1.2.0 was going to become 2.0, and it would be available at a $359.95 Savings. Essentially the savings are from:

  • $110 discount off the normal $249 Kwolla 2.0 Pricing
  • Free 2 hour premium support, which is valued at $200
  • Free installation of Kwolla 1.1.x valued at $49 (yes same one I mentioned above)

The fact that Kwolla 2.0 will have an admin panel and web-based installer is particularly nice, however the admin panel is something I feel that should have been included since 1.0.

Now being the facetious type of person I am I shot back a remark regarding the lack of a administrative panel, particularly at the $249 pricing which happens to be the same price as SocialEngine.

When you can get back to me about some of the most basic features like an
admin panel, and so forth, then I'll consider Kwolla even worth close to
that of socialengine.net pricing.
 
-Karl

Mind you while SocialEngine is more complicated, and I do not really care for it, most of the features like ‘photo albums’, music, videos, so forth are ‘extras’ on top of SE’s $249 pricing. So in that sense Kwolla did have a competitive advantage. However as much as I loathe SocialEngine (clients use them, I do not), I would not have expected the following reply from them in the same circumstances.

Karl,
 
Kwolla 2.0 will have an admin panel. I prefer you never use my
software or contact me about it again. Please use Social Engine, Elgg,
Dolphin, or any of the other pieces of social network software out
there.
 
-Vic

The software itself shows plenty of promise. And the free version 1.1.1 can be a great starting point for any developer wishing to develop a custom social networking site, assuming they downloaded it during the promotional period. However such a response carries a heavy reflection on what you might expect from customer support down the road if you are dissatisfied with the product. Granted I did receive the product for free, and I was better equipped at trouble shooting issues myself, such responses are all too common of signs of things to come.

Can you imagine what a typical end user would do if faced with a blank screen due to older software? There goes the 2 hour premium support over such a simple issue. Also I hope for his customer’s sake that when 2.0 is available the migration from 1.1.1 will be quite painless, and that he doesn’t receive plenty of emails asking where the admin panel is prior to 2.0′s release.

It’s a shame too, because I know plenty of clients who would like to escape from scripts like SocialEngine and others.

Nginx and Codeigniter The Easy Way

March 5th, 2011

I personally don’t use Codeigniter (was never much of a fan of PHP frameworks), however I had a client approach me with this issue so I decided to take a stab at it.

Now normally you would look for any existing .htaccess that comes with a script package and attempt to convert the rewrite rules. But seriously though, why do we really want to go thru all that fuss when we can simply use try files:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /path/to/your/website.com/;
 
    location / {
        try_files $uri $uri/ /index.php;
    }
 
    # For more information on the next two lines see http://kbeezie.com/view/nginx-configuration-examples/
    include php.conf;
    include drop.conf;
}

In a nutshell this should normally work… but why does it not?

Quite simple really; CodeIgniter by default uses PATH_INFO which is a really antiquated method of parsing the uri. As a result we must tell CI to use REQUEST_URI instead. To do this open up your config.php under /system/application/config/ and find the $config['uri_protocol'] and set it to this:

$config['uri_protocol'] = "REQUEST_URI";

You could also choose to use AUTO, but since we know we’ll be dealing with request_uri, it is best to set it as such (though if you do run into problems, give AUTO a try).

If you have not already set the index page, you will want to blank it out in order for it to work with a rewrite method (request_uri, etc).

$config['index_page'] = "";

For known static files, take it a step further and capture common static requests so that you can cache accordingly:

	location ~* \.(ico|css|js|gif|jpe?g|png)$ {
		expires max;
		add_header Pragma public;
		add_header Cache-Control "public, must-revalidate, proxy-revalidate";
	}

The Importance of Caching WordPress

February 16th, 2011

Update: The last page has been updated with some W3 Total Cache Rules if you would like to use W3 Total Cache with File-based caching (disk enhanced). Performance wise, it should be about the same as WP-Super-Cache that you see here, but maybe a bit better since you’ll also get the benefits of database/object caching behind it

IMPORTANT: This article is NOT a Wp-Super-cache vs W3-Total-Cache comparison, or standoff. This article is simply to illustrates the benefits of using a caching plugin with your WordPress installation and how it can save you not only hits, but resources on your server. Besides it has some pretty looking graphs that don’t really mean much for you to look at…

WordPress, in all its glory… is a damn sloppy hog.

I use WordPress, many of my clients use WordPress, a lot of you reading this probably use WordPress as well. But in it’s simplicity of installation, and vast library of plugins, themes and other addons, it is quite simply a resource hog. A simple one-post blog would easily rack up the PHP memory usage and still be quite slow in handling multiple visitors.

If you are hosting clients using wordpress, you cannot tell me that not one of them didn’t approach you about increasing their memory limit, or their site being incredibly slow after being hit with a moderate amount of traffic.

But despite all that, I like WordPress. It’s easy to use, update and modify; even I want to be lazy at times.

The Setups

For the purpose of this article, I cloned my blog to three different subdomains onto three separate VPSes with the same base configuration. A Debian squeeze 6.0 based installation, with 512MB assigned, and running Nginx 0.9.4, PHP-FPM 5.3.5, and MySQL 5.1.

All three had the following plugins installed:

  • Google XML Sitemaps
    Automatically generates sitemap xmls and reports new postings to search engines like Google.
  • Mollom
    Alternative to Akismat plugin, which catches spam comments.
  • WP-Syntax
    Plugin that uses GeSHi to provide syntax highlighting. Something I use quite a bit on this site for code examples.
  • WP-Mail-SMTP
    Plugin to use an SMTP server instead of php’s built in mail() function.

As you can see, nothing too fancy, pretty basic.

No Caching

This setup is basically only the above plugins, no additional caching option turned on other than what would come with wordpress (which seems to be none…)

W3 Total Cache w/ memcached

This setup has the W3 Total Cache installed, with all of the caching options (Page, Minify, Database, Object) set to use a local memcache daemon with 32MB of memory set (all of the objects seemed to need no more than 25MB). Some of the stuff suggested by Browser Cache is already handled by nginx such as mime types and a location block to set an expiration header to static content.

Memcached was installed from the normal Debian repository, and the memcache extension for PHP installed via pecl.

This type of setup is ideal for those who really don’t want to hassle with the nginx configuration aside from the usual optimization you apply to static content, and the one-line-try-files solution to WordPress’ permalink feature. It also makes it very easy to clear the cache when you make a change.

WP Super Cache

Doesn’t cache things like the database or performs minifying operations. I simply set this one up with disk caching the files to a static folder. Nginx was configured to look in these folders and serve off any static cache that was found. I’ve tested this with both gzip precompression enabled on nginx (serves a .gz file if it exists), as well as having nginx handle the gzip compression on the fly.

Essentially with this setup, most visitors rarely actually make it to the PHP backend (as the php pages themselves are cached as html output onto the disk).

This setup is ideal if you need to spare PHP from running as much as possible. Logging into the back of course will still have that little spike of usage. And of course with PHP rarely being accessed you’ll likely need to setup a cron job for scheduled postings and other maintainance to wp-cron.php.

PS: W3 Total cache can also use disk-caching on top of the additional features it offers, but I figured It’d be nice to show how much memcache can help even with every user visit being sent to the PHP backend.

The Method of Testing

Testing was done very simply with an application called Siege. Each scenario was tested separately, so that the environment conditions they were in were as close as possible (it would be kind of silly to try to run a test on all three at the same time if they were on the same physical server despite being in separate VPS containers).

Each scenario was hit with a surge of connections, ranging from 5 concurrent browsers, to 150 concurrent browser. Each Siege test was run for 5 minutes, with a pause of 4 minutes in between. Meaning starting from 5 concurrent browsers, it would run for 5 minutes hitting up the destination as much as possible, then pause for 4 minutes before trying again with 10, then 20, then 30 then 40 concurrent browsers and so forth.

An example of the command would be:

seige -c# -t5M -b http://destination

Where # is the number of concurrent browsers to use, and b sets it in benchmark mode, allowing no delays between connection attempts. This is not very realistic in terms of actual internet traffic which tends to have some delay, and connections occur at random intervals rather than all-out-slashdotting/digg (which is still technically random, but just more intense). The idea here is to illustrate just how much load WordPress could theoretically take under a large amount of traffic.

The “attacking” server was another VPS, so some of these numbers would rarely be possible in the real world. Normally most users on shared or VPS hosting don’t normally have access to higher than a 10mbit uplink, let alone the next-to-nothing latency you get between two VPSes on the same box, so essentially we’re breaking it down to where the webserver, PHP and database server become the main bottleneck.

Also keep in mind, the test does not attempt to download anything other than the front page html, meaning additional requests for images, css, js were not retrieved. Just simply the download of the front page, which is far less than what a normal browser would download off a typical blog.

On the following pages are all the pretty graphs and numbers between each scenario.

IPv6 with OpenVz (venet)

February 16th, 2011

This article assumes that the physical node that is setup with OpenVz already has Native IPv6 setup and running, and that you are using OpenVz containers connected with a venet device. For those who need help getting their physical node setup with IPv6 on CentOS/RHEL click here.

Ideally IPv6 works best over a veth adapter, as they have a MAC address and play much more nicely with auto configuration within the container. However not all of us have the luxury of getting an OpenVz container with a bridged veth adapter AND IPv6.

In a nutshell, using a venet adapter suffers the following:

venet devices are not fully IPv6 compliant, but still works if you statically assign IPv6 addresses. They do not properly support MAC addresses and consequently link local addresses and can not play nice with neighbor discovery or router advertisements, router discovery, or auto-conf. They also require additional modifications to the layer 3 forwarding behaviour of the host via sysctl.

The wiki of course tells you how to add an IPv6 address to a container, but doesn’t go into details on actually getting it to work. I only know of these steps to work on a CentOS and Debian container.

It’s important that the following has been set on the physical server for the following guide to work

sysctl net.ipv6.bindv6only 1

Adding an IP

In the case of the server kbeezie.com is on, I have a /64 block assigned to me by my datacenter. For those not familiar, a /64 block equates to about 18 quintillion addresses. In SolusVm you normally add a range of IPv6 addresses by randomly generating about a 100 addresses at a time. But you can also manually define an address like I did with kbeezie, choosing 2001:4858:aaaa:6::6 for this site. The :: represents an area that is filled with zeros, as such the long address would be 2001:4858:aaaa:0006:0000:0000:0006.

Now to add this address to a container we would simply use vzctl:

vzctl set <id> --ipadd <ipv6_addr> --save

For example if your containers’ CTID is 101, and you wanted to add 2001:4858:aaaa:6::1, you would perform the following:

vzctl set 101 --ipadd 2001:4858:aaaa:6::1 --save

Verifying IPv6 Connectivity

Now when you log into your container, you should be able to perform the following:

ping6 ipv6.google.com

If you get an error that there’s no connection to host, or you simply time out, then first double check that the IPv6 address shows in your ifconfig output. If it’s there then try the following command to add a default route from the container to the host node:

On CentOS/RHEL container:

ip route add 2000::/3 dev venet0

On Debian/Ubuntu container:

ip route add ::/0 dev venet0

You can verify the default route has been added with:

ip -6 route show

Once that has been added, then try to ping6 ipv6.google.com once a gain. If you see ping responses then you’re done with that. If not move onto the next step.

Adding route to container at physical node

Sometimes when you add an IP address to a container, the traffic simply doesn’t make it to the containers. I’ve had this annoyance a number of times especially when I add an IP address to a container in SolusVm.

Lets say for example you added 2001:4858:aaaa:6::1 to a container, but traffic simply is not making it thru. You may need to re-add the route between the container and the eht0 adapter. To do this use the following command:

ip -6 neigh add proxy 2001:4858:aaaa:6::1 dev eth0

Once thats done, log back into the container and try to ping6 ipv6.google.com again. At this point you should be getting a response.

Configuring Web Servers

As mentioned in the openvz wiki, link local and other features will not be available with a venet adapter. A prime example of this is listening on all available IPv6 interfaces such as [::]:80 will not work in most cases as the software will often complain of the address already being bound. Likewise any services that can listen on both IPv4 and IPv6 will need to be set to only handle IPv6 traffic over IPv6 (since its possible for IPv6 to handle both IPv6 and IPv4 traffic).

It helps to have Google’s nameservers ( 8.8.8.8 and 8.8.4.4 ) in your /etc/resolv.conf if you do not already have a IPv6-friendly nameserver configured.

The following are tweaks you can use to make IPv6 work in various webservers.

Nginx

Before you can use IPv6 with Nginx, the webserver needs to be compiled with the –with-ipv6 option. You may already have this option enabled, you can check by calling nginx -V. To listen on IPv6 you can add the following line to your server block:

listen [2001:4858:aaaa:6::1]:80 ipv6only=on;

Remember that each server block will need to have it’s own unique IPv6 address and that you cannot bind to all [::] interfaces. The option ipv6only=on needs to be enabled in order to play nicely with venet. If the physical node already has ipv4 binding turned off then ipv6only may not be necessary, but its a good idea to use anwyays with the venet adapter. To ensure that a server block is listening on both IPv6 and IPv4 you may need to have the following:

listen 0.0.0.0:80;
listen [2001:4858:aaaa:6::1]:80 ipv6only=on;

You can also choose to listen on a specific IPv4 address. The listen line for IPv4 is needed, since otherwise the server block will only be listening on IPv6.

More information on Nginx’s listen directive can be found in the Wiki.

Lighttpd

I’m not entirely familiar with lighttpd myself, but it does share a similar directive to nginx for forcing IPv6 listeners to only handle IPv6 traffic. IPv6 setup is outlined in Wiki overview. Which shows V6_ONLY is already enabled in most common cases where you have to statically define the IP address.

Again you cannot bind to [::] when using venet on OpenVz.

Apache/httpd

Apache does not have a v6-only type of directive. Instead you have to recompile apache with –disable-v4-mapped. Once Apache has been compiled you can use the [IPv6]:80 format, you’ll need to have a Listen directive set to your new IPv6 address, as well as any virtual hosts. Like with both nginx and lighttpd above, you cannot bind to all interfaces ([::]), only specifically assigned addresses.

Control panels, Firewalls, etc

Firewall software such as UFW (uncomplicated firewall by ubuntu), and such do not play nicely with IPv6 and OpenVz (over venet). So thats something to keep in mind when setting up firewall software that rely on kernel modules and such that are not often available in a virtualized environment (Xen is typically more suited for something like that).

Not all control panels are IPv6 ready. DirectAdmin for example does have the option to enable IPv6, and will let you manage IPv6 addresses in the IP configuration and DNS area. However it does not configure the services like apache, bind9, dovecot and so forth for you. For those services you’ll have to configure manually.

Conclusion

For the purpose of hosting websites and logging into my server via SSH, the above guidelines work just fine. I currently host kbeezie.com, kbeezie.net (static CDN), ionvz.com and a few other websites behind IPv6 with Nginx on such a configuration.

If you do not have IPv6 connectivity from your home or office machine, you can use a free tunnnel service such as HE.net’s IPv6 Tunnel Broker Service. which I’m using here at home. I currently use Hurricane Electric for my servers backbone (native IPv6 for my servers), Home IPv6 Tunneling, and Free DNS Service which seems to work much better than using my domain registrar’s DNS.

Protecting Folders with Nginx

February 5th, 2011

This question comes up every so often, and its actually fairly easy besides the fact you do not use an .htaccess file. To do so we’ll need the auth_basic module which comes included with Nginx.

First we’ll need to create a password file, you can create this in the folder you wish to protect (though the file can reside anywhere Nginx has access to).

Creating .htpasswd

If you previously had apache/httpd installed on your machine you may still have some of the utilities that came with it. In this case we’ll want to use the htpasswd command:

htpasswd -c /var/www/domain.com/admin/.htpasswd username

When you run the above command, it will prompt you for a password for the provided username, and then create the file .htpasswd in the folder you specified. If you already have a pre-existing password file, you can omit the -c flag. You can use the -D flag to remove the specified user from a password file.

If you do not have htpasswd on your system, you can create the file yourself which each line in the format of:

username:encrypted-password:comment

To generate the encrypted password you can use Perl which is installed on most systems.

perl -le 'print crypt("your-password", "salt-hash")'

Or you can use Ruby via the interactive console command ‘irb’:

"your-password".crypt("salt-hash")

Setting up Nginx

Add protection for the /admin folder.

server {
    listen 80;
    server_name domain.com www.domain.com;
 
    location / { 
        # your normal content
    }
 
    location /admin {
        auth_basic "Administrator Login";
        auth_basic_user_file /var/www/domain.com/admin/.htpasswd;
    }
 
    #!!! IMPORTANT !!! We need to hide the password file from prying eyes
    # This will deny access to any hidden file (beginning with a .period)
    location ~ /\. { deny  all; }
}

With the above access to the admin directory will prompt the user with a basic authentication dialog, and will be challenged against the password file provided.

Additional Security Considerations

The password file itself does not have to be named .htpasswd, but if you do store it a web-accessible location make sure to protect it. With the last location block above using any name with a period in front should be protected from web-access.

The safest place to store a password file is outside of the web-accessible location even if you take measures to deny access. If you wish to do so, you can create a protected folder in your nginx/conf directory to store your password files (such as conf/domain.com/folder-name/password) and load the user file from that location in your configuration.

Deploying circuits.web with Nginx/uwsgi

January 31st, 2011

I’m a very minimal person when it comes to frameworks, I don’t generally like something that needs to generate an entire application file structure like you’d see with Django. When I was searching around for various frameworks to get me started with python and web development, I investigated the usual; DJango, CherryPy, Web.Py. I fell in love with circuits due it’s ease and simplicity, yet it can be quite powerful. This article will show you how to get Nginx setup with uWSGI along with a sample circuits.web application.

Nginx, as of version 0.8.40, comes deployed with the uwsgi module unless otherwise excluded (ie: –without-http_uwsgi_module), the older legacy version 0.7.63 and above came with the module, but needed to be compiled into it. The syntax I use in this article conforms to 0.8.40 and newer and might not work with an older version of Nginx. (Currently I’m using this on Nginx 0.9.4, with Python 2.6.6)

This article assumes you already have Nginx 0.8.40+ and Python installed.

Installing the uWSGI server

As stated on their wiki, “uWSGI is a fast (pure C), self-healing, developer/sysadmin-friendly application container server.”, it utilizes the uwsgi protocol (notice the all-lowercase spelling), and supports WSGI applications served from it.

Once you’ve downloaded the source from the wiki, you can install it (a number of methods here). For my purpose I’ve moved the compiled uwsgi binary to my /usr/local/bin folder so that I could call it from anywhere on my system.

Other than FreeBSD I have not seen uWSGI readily available in most distribution’s package systems.

You can test your installation by moving out of the source folder, and calling the binary:

# uwsgi --version
uWSGI 0.9.6.7

Simple Hello world app without daemonizing uWSGI

Now we’re going to setup a very simple WSGI hello world application, and host it behind uWSGI and use Nginx to serve it. We’re not going to daemonize the uWSGI as such you’ll see it’s output in your terminal as connections are made. Also in Nginx we’ll simply send everything to the backend application for this demonstration.

Configure Nginx

server {
	server_name myapp.example.com;
 
	location / { 
		uwsgi_pass 127.0.0.1:3031;
		#You can also use sockets such as : uwsgi_pass unix:/tmp/uwsgi.sock;
		include uwsgi_params;
	}
}

If Nginx is currently running, either restart it, or you can reload the configuration with “nginx -s reload”.

Create a WSGI application
In your application folder create a python file, for example myapp.py:

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return ["Hello World!"]

Deploy with uWSGI
Now we’ll want to deploy a simple single-worker uWSGI instance to serve up your application from your application folder:

# uwsgi -s 127.0.0.1:3031 -w myapp
*** Starting uWSGI 0.9.6.7 (64bit) on [Mon Jan 31 00:10:36 2011] ***
compiled with version: 4.4.5
Python version: 2.6.6 (r266:84292, Dec 26 2010, 22:48:11)  [GCC 4.4.5]
uWSGI running as root, you can use --uid/--gid/--chroot options
 *** WARNING: you are running uWSGI as root !!! (use the --uid flag) *** 
 *** WARNING: you are running uWSGI without its master process manager ***
your memory page size is 4096 bytes
allocated 640 bytes (0 KB) for 1 request's buffer.
binding on TCP port: 3031
your server socket listen backlog is limited to 100 connections
initializing hooks...done.
...getting the applications list from the 'myapp' module...
uwsgi.applications dictionary is not defined, trying with the "applications" one...
applications dictionary is not defined, trying with the "application" callable.
application 0 () ready
setting default application to 0
spawned uWSGI worker 1 (and the only) (pid: 20317)

If you attempt to connect to the site, and all goes well you’ll see Hello World on the screen as well as some log outputs on your terminal. To shut down the uWSGI server use Ctrl+C on that screen.

Now for some fun with Circuits.web

Circuits is a Lightweight Event driven Framework for the Python Programming Language, with a strong Component Architecture. It’s also my favorite framework for deploying very simple web applications (but can be used for far more complicated needs, for example SahrisWiki).

For this article I’ll show you a few ways circuits.web can really simplify handling requests and URI parsing.

First thing we’ll need to do is get the circuits module installed into Python.

If you do not already have Python SetupTools, install them:

apt-get install python-setuptools

And then simply install via easy_install:

easy_install circuits

You can get the egg (2.6 only) or the source of Circuits 1.3.1 from PyPi.Python.org.

On to page 2…

Apache and Nginx Together

January 30th, 2011

I’m not going to get into a lot of details about how to install and configure either http server from scratch. This article is primarily going to be food for thought for those who may want or need to configure nginx along side an existing apache (httpd) configuration. If you would rather ditch Apache all together, consider reading Martin Fjordvald’s Nginx Primer 2: From Apache to Nginx.

Side by Side

In some cases you may need to run both Apache (httpd) and Nginx on port 80. Such a situation can be a server running Cpanel/Whm and as such doesn’t support nginx, so you wouldn’t want to mess with the apache configuration at all.

To do this you have to make sure Apache and Nginx are bound to their own IP adddress, In the event of WHM/Cpanel based webserver, you can Release an IP to be used for Nginx in WHM. At this time I am not aware of a method of reserving an IP, and automatically forcing Apache to listen on a specific set of IPs in a control panel such as DirectAdmin or Plesk. But the link above will show you how with WHM/Cpanel.

Normally Apache will be listening on all interfaces, and such you may see a line like this in your httpd.conf file:

Port 80
#or
Listen 80

Instead you’ll need to tell apache to listen on a specific IP (you can have multiple Listen lines for multiple IPs)

Listen 192.170.2.1:80

When you change Apache to listen on specific interfaces some VirtualHost may fail to load if they are listening on specific IPs themselves. The address option in <VirtualHost addr[:port] [addr[:port]] …> only applies when the main server is listening on all interfaces (or an alias of such). If you do not have an IP Listed in your <VirtualHost …> tag, then you do not need to worry bout this. Otherwise make sure to remove the addr:port from the VirtualHost tag.

Make sure to update the NameVirtualHost directive in Apache for name-based virtual hosts.

Once you have apache configured to listen on a specific set of IPs you can do the same with nginx.

server {
    listen 192.170.2.2:80;
    ...
}

Now that both servers are bound to specific IPs, both can then be started up on port 80. From there you would simply point the IP of the domain to the server you wish to use. In the case of WHM/Cpanel you can either manually configure the DNS entry for the domain going to nginx in WHM, or you can use your own DNS such as with your registrar to point the domain to the specific IP. Using the latter may break your mail/ftp etc configurations if the DNS entries are not duplicated correctly.

Though normally if you are setting up Nginx side-by-side with apache, you’ll have your domain configurations separate from the rest of the system. The above paragraph only applies if the domain you are using has already been added by cpanel/whm and you wish to have it served by nginx exclusively. [Scroll down for PHP considerations]

Apache behind Nginx

If you are using a control panel based hosting such as cpanel/whm, this method is not advised. Most of the servers configuration is handled automatically in those cases, and making manual changes will likely lead to problems (plus you won’t get support from the control panel makers or your hosting provider in most cases).

Using Nginx as the primary frontend webserver can increase performance regardless if you choose to keep Apache running on the system. One of Nginx’s greatest advantage is how well it serves static content. It does so much more efficiently than Apache, and with very little cost to memory or processing. So placing Nginx in front will remove that burdern off Apache, leaving it to concentrate on dynamic request or special scenarios.

This method is also popular for those who don’t want to use PHP via fastcgi, or install a separate php-fpm process.

First thing that needs to be done is to change the interface apache listens on:

Listen 127.0.0.1:8080

Above we bind Apache to the localhost on an alternate port, since only Nginx on the same machine will be communicating with Apache.

Note: Since Nginx needs to access the same files that Apache serves, you need to make sure that Nginx is setup to run as the same user as apache, or to make sure that the Nginx’s selected user:group has permission to read the web documents. Often times the webroot is owned by nobody or www-data and Nginx likewise can be setup to run as those users

Then in Nginx listening on port 80 (either on all interfaces such as 0.0.0.0 or to specific IPs, your choice) we would need to configure Nginx to serve the same content. Take for example this virtual host in an apache configuration:

<VirtualHost>
      DocumentRoot "/usr/local/www/mydomain.com"
      ServerName mydomain.com
      ServerAlias www.mydomain.com
      CustomLog /var/log/httpd/mydomain_access.log common
      ErrorLog /var/log/httpd/mydomain_error.log
      ...
</VirtualHost>

In Nginx the equivalent server configuration would be:

server {
      root /usr/local/www/mydomain.com;
      server_name mydomain.com www.mydomain.com;
 
      # by default logs are stored in nginx's log folder
      # it can be changed to a full path such as /var/log/...
      access_log logs/mydomain_access.log;
      error_log logs/mydomain_error.log;
      ...
}

The above would serve all static content from the same location, however PHP will simply come back as PHP source. For this we need to send any PHP requests back to Apache.

server {
      root /usr/local/www/mydomain.com;
      server_name mydomain.com www.mydomain.com;
 
      access_log logs/mydomain_access.log;
      error_log logs/mydomain_error.log;
 
      location / { 
            # this is the location block for the document root of this vhost
      } 
 
      location ~ \.php {
            # this block will catch any requests with a .php extension
            # normally in this block data would be passed to a FastCGI process
 
            # these two lines tell Apache the actual IP of the client being forwarded
            # Apache requires mod_proxy (http://bit.ly/mod_proxy) for these to work
            # Most Apache 2.0+ servers have mod_proxy configured already
 
            proxy_set_header X-Real-IP  $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
 
            # this next line adds the Host header so that apache knows which vHost to serve
            # the $host variable is automatically set to the hostname Nginx is responding to
 
            proxy_set_header Host $host;
 
            # And now we pass back to apache
            # if you're using a side-by-side configuration the IP can be changed to
            # apache's bound IP at port 80 such as http://192.170.2.1:80
 
            proxy_pass http://127.0.0.1:8080;
      }
 
       # if you don't like seeing all the errors for missing favicon.ico in root
       location = /favicon.ico { access_log off; log_not_found off; }
 
       # if you don't like seeing errors for a missing robots.txt in root
       location = /robots.txt { access_log off; log_not_found off; }
 
       # this will prevent files like .htaccess .htpassword .secret etc from being served
       # You can remove the log directives if you wish to
       # log any attempts at a client trying to access a hidden file
       location ~ /\. { deny all; access_log off; log_not_found off; }
}

In the case of rewriting (mod_rewrite) with apache behind nginx you can use an incredibly simple directive called try_files. Here’s the same code as above, but when changes made so that any files or folder not found by nginx gets passed to apache for processing (useful for situations like WordPress, or .htaccess based rewrites when the file or folder is not caught by Nginx)

server {
      root /usr/local/www/mydomain.com;
      server_name mydomain.com www.mydomain.com;
 
      access_log logs/mydomain_access.log;
      error_log logs/mydomain_error.log;
 
      location / { 
            # try_files attempts to serve a file or folder, until it reaches the fallback at the end
            try_files $uri $uri/ @backend;
      } 
 
      location @backend {
            # essentially the same as passing php requests back to apache
 
            proxy_set_header X-Real-IP  $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Host $host;
            proxy_pass http://127.0.0.1:8080;
      }
      # ... The rest is the same as the configuration above ... 
}

With the above configuration, any file or folder that exists will be served by Nginx (the php block will catch files ending in .php), otherwise it will be passed back to apache on the backend.

In some cases you may wish for everything to be passed to Apache unless otherwise specified, in which case you would have a configuration such as this:

server {
      root /usr/local/www/mydomain.com;
      server_name mydomain.com www.mydomain.com;
 
      access_log logs/mydomain_access.log;
      error_log logs/mydomain_error.log;
 
      location / {
            proxy_set_header X-Real-IP  $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
 
            proxy_set_header Host $host;
 
            proxy_pass http://127.0.0.1:8080;
      }
 
      # Example 1 - Specify a folder and it's content
      # To serve everything under /css from Nginx-only
      location /css { }
 
      # Example 2 - Specify a RegEx pattern such as file extensions
      # Serve certain files directly from Nginx
      location ~* ^.+\.(jpg|jpeg|gif|png|css|zip|pdf|txt|js|flv|swf|html|htm)$
      {
            # this will basically match any file of the above extensions
            # course if the file does not exist you'll see an Nginx error page as opposed
            # to apache's own error page. 
      }
}

Personally I prefer to leave Nginx to handle everything, and specify exactly what needs to be passed back to Apache. But the last configuration above would more accurately allow .htaccess to function during almost-every request since almost everything gets passed back to apache. The css folder or any files found by the other location block would be served directly by nginx regardless if there’s an .htaccess file there, since only Apache processes .ht* files.

PHP Considerations

If you are not familiar with Nginx, then it should be noted that Nginx does not have a PHP module like apache’s mod_php, instead you either need to build PHP with FPM (ie: php-fpm/fastcgi), or you need to pass the request to something that can handle PHP.

In the case of using Nginx with Apache you essentially have two choices:

1) Compile and Install PHP-FPM, thus running a seperate PHP process to be used by Nginx. This option is usually good if you want to keep the two webserver configurations separated from each other, that way any changes to one won’t affect the other. But of course it adds additional memory usage to the system.

2) Utilize Apache’s mod_php. This option is ideal when you will be passing data to apache as a backend. Even in a side-by-side scenario, you can utilize mod_php by proxying any php request to Apache’s IP. But in the side-by-side scenario you have to make sure that Apache is also configured to serve the same virtualhost that Nginx is requesting.

Speed-wise both methods are about the same when PHP is configured with the same set of options on both. Which option you choose depends solely on your needs. Another article that may be of interest in relations to this one would be Nginx as a Proxy to your Blog, which can be just as easily utilized on a single server (especially if you get multiple IP ranges like I do).