Setting up a Linux server for WordPress/Drupal/Joomla

This is a practical guide to setting up a server for maximum performance running one of the blog-oriented CMS platforms: WordPress, Drupal, or Joomla.

I’ll cover the setup on a Linux Ubuntu server. You’ll need Nginx, PHP, MySQL, and Varnish.

1. MySQL

Starting from the end. The default MySQL settings are fine with me, so just install it:

apt-get install mysql-server-5.5 mysql-client-5.5

2. PHP

PHP needs to run in fast-cgi mode. NginX doesn’t have a module like libapache-modphp.
So you’ll need php-cli, php-common, and the rest. If you’re paranoid and want the latest php version, use this article and build it from source.

The version available in the repos works fine:

apt-get install php5-cgi php5-cli php5-common php5-curl php5-gd php5-json php5-mysql php5-readline spawn-fcgi

Download the init.d script. For Ubuntu it’s different from the one shown in this article

wget -O /etc/init.d/php-fastcgi /wp-content/uploads/2014/11/php-fastcgi-init

Also download the fastcgi handler itself:

wget -O /usr/bin/php-fastcgi /wp-content/uploads/2014/11/php-fastcgi-bin

Make them executable:

chmod +x /etc/init.d/php-fastcgi  
chmod +x /usr/bin/php-fastcgi

Add php to startup:

sudo update-rc.d php-fastcgi defaults 80

Start it:

/etc/init.d/php-fastcgi start

Check:

netstat -tunlp |grep 9000

If php-fcgi started, you’ll get this output:

tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      6806/php5-cgi

3. Nginx

I’d use the version available in the repos. If you’re paranoid and want the latest version, build it from source using this article.

Install:

apt-get install nginx

Create a config file for the site:

nano /etc/nginx/sites-available/**mywebsite.com**

Put this in it:

server {
    listen 8080;
    server_name mywebsite.com www.mywebsite.com;

    root /var/www/html;

    index index.php;

    if ($host ~ !^(mywebsite.com|www.mywebsite.com)$) {
        rewrite ^ http://www.mywebsite.com$request_uri? permanent;
    }

    # Limit methods, allowed on server to GET, HEAD and POST
    if ($request_method !~ ^(GET|HEAD|POST)$ ) {
        return 444;
    }

    location ~* \.(ico|gif|jpeg|jpg|png|eot|ttf|swf|woff)$ {
        root /var/www/html;
        expires 30d;
        access_log off;
    }

    location ~* \.(css|js)$ {
        root /var/www/html;
        expires 7h;
        access_log off;
    }

    location / {
        try_files $uri $uri/ /index.php;
    }

    location ~ \.(php|html)$ {
        root /var/www/html;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

The sensitive spots of the blog are better locked down with basic auth.

WordPress:

location /wp-admin {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/auth/.ht.passwd_admin;
    }
    location ~* /wp-login.php {
        try_files $uri $uri/ $uri/wp-login.php /wp-login.php;
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/auth/.ht.passwd_admin;
        root /var/www/html;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;

    }
    location ~* /xmlrpc.php {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/auth/.ht.passwd_admin;

        try_files $uri $uri/ $uri/xmlrpc.php /xmlrpc.php;
        root /var/www/html;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;

    }

Joomla:

location /administrator {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/auth/.ht.passwd_admin;
    }

Drupal:

location /admin {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/auth/.ht.passwd_admin;
    }

To create the passwords file we need apache2-utils. Install it:

apt-get install apache2-utils

Create the /etc/nginx/auth/.ht.passwd_admin file (the /etc/nginx/auth folder doesn’t exist yet):

mkdir /etc/nginx/auth  
htpasswd -cmb /etc/nginx/auth/.ht.passwd_admin **user password**

Edit the main config file:

nano /etc/nginx/nginx.conf

Make the following changes to it:

Change the user to www-data:

user www-data;

Improve security:

client_body_buffer_size 4K;
        client_header_buffer_size 4k;
        large_client_header_buffers 4 4k;
        limit_conn_zone $binary_remote_addr zone=slimits:5m;

This next block lets you see visitors’ real ip addresses in the log files:

##
        # Logging Settings
        ##
        set_real_ip_from   127.0.0.1;
        real_ip_header X-Forwarded-For;

        log_format   main '$remote_addr - $remote_user [$time_local]  $status '
            '"$request" $body_bytes_sent "$http_referer" '
            '"$http_user_agent" "$http_x_forwarded_for"';

        log_format forwarded '"$http_x_forwarded_for" - $remote_user [$time_local]  $status '
            '"$request" $body_bytes_sent "$http_referer" '
            '"$http_user_agent"';

        access_log /var/log/nginx/access.log forwarded;
        error_log /var/log/nginx/error.log;

The final step is setting up file compression:

##
        # Gzip Settings
        ##

        gzip on;

        gzip_disable "msie6";

        gzip_vary on;
        # gzip_proxied any;
        gzip_comp_level 6;
        gzip_buffers 64 8k;
        gzip_http_version 1.1;
        gzip_types text/plain text/html text/css application/json application/x-javascript text/xml application/xml alication/xml+rss text/javascript;

Make sure the /var/www/html folder exists:

mkdir -p /var/www/html

Last thing is to enable the site:
ln -s /etc/nginx/sites-available/mywebsite.com /etc/nginx/sites-enabled/mywebsite.com

and restart nginx for the changes to take effect:

service nginx restart

Nginx setup is done. Once started it’ll be available on port 8080. Left to set up Varnish so it accepts incoming connections on port 80.

4.Varnish

Install Varnish

apt-get install varnish

I covered Varnish configuration in detail in [A look at Varnish configuration](http://www.tech-notes.net/varnish-configuration-rewiev/), so I won’t chew through it again here.

First, edit /etc/default/varnish and bring the DAEMON_OPTS section to this shape:

DAEMON_OPTS="-a :80 \
             -T localhost:6082 \
             -t 120\
             -f /etc/varnish/website.vcl \
             -S /etc/varnish/secret \
             -s malloc,256m"

Next, create the /etc/varnish/website.vcl file and fill it in:

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

acl purge { "localhost"; }

sub vcl_recv {

    set req.http._sess = regsub( regsub( req.http.Cookie, ".*PHPSESSID=", "" ), ";.*", "" );

    if (req.request == "PURGE") {
      if (!client.ip ~ purge) {
        error 405 "Not allowed.";
      }
    }

     if (req.restarts == 0) {
        if (req.http.x-forwarded-for) {
            set req.http.X-Forwarded-For = req.http.X-Forwarded-For;
        } else {
            set req.http.X-Forwarded-For = client.ip;
        }
    }

    if (req.request != "GET") { return (pipe); }

    if (req.url == "^/(wp-admin|wp-login|admin|administrator)") {
	return (pipe);
    }

    if (req.url ~ "^/(/wp-content|media|images|graphics)/.*\.(ico|gif|jpeg|jpg|png|eot|ttf|swf|woff)$") {
	unset req.http.Cookie;
	return (pipe);
    }


    if (req.http.Authorization || req.request == "POST" || req.http.Authenticate)
    {
	return (pass);
    }

    set req.backend = default;
    return (lookup);
 }
sub vcl_fetch {
    # Remove Expires from backend, it's not long enough
    unset beresp.http.expires;
    # Set the clients TTL on this object
    set beresp.http.cache-control = "max-age=900";
    # Set how long Varnish will keep it (1 minute)
    set beresp.ttl = 3600 s;

     return (deliver);
 }

sub vcl_deliver {
    remove resp.http.Via;
    remove resp.http.Age;
    remove resp.http.X-Purge-URL;
    remove resp.http.X-Purge-Host;
    remove resp.http.X-CF-Powered-By;

    return (deliver);
}

Pay attention to this construct in the vcl_recv section

if (req.url ~ "^/(/wp-content|media|images|graphics)/.*\.(ico|gif|jpeg|jpg|png|eot|ttf|swf|woff)$") {
	unset req.http.Cookie;
	return (pipe);
    }

This is what routes static file requests straight to Nginx, bypassing varnish. If your site keeps images in a pictures folder, change this section to look like this:

if (req.url ~ "^/(/wp-content|media|images|graphics|pictures)/.*\.(ico|gif|jpeg|jpg|png|eot|ttf|swf|woff)$") {
	unset req.http.Cookie;
	return (pipe);
    }

This way Varnish won’t cache static requests, non-GET requests, auth requests, or any admin panel access.

Restart Varnish for the changes to take effect:

service varnish restart

I’d recommend installing plugins that flush the Varnish cache when blog posts get updated. Such plugins exist for pretty much every CMS.

Wrapping up:

With this setup, WordPress on a server with 1GB RAM, 1CPU handles 500+ users per second. Couldn’t generate more within the free plan on blazemeter.com.

Now you can set up an ftp server and create virtual users, then upload content and install the CMS.

Also recommend setting up backups for your sites.

For extra peace of mind you can set up fail2ban

Once the content is uploaded, I’d recommend optimizing the images to speed up image delivery.