Old habits die hard. I still love Firefox, and I’m still in love with apache2, but new times are coming. While I was hosting I needed a solution that could handle sites with different users, to log with different user rights and to secure the code from each other as hard as possible. I achieved this with unix users, 0700/0600 dir/files rights and with apache2-mpm-itk, and it did work like charm. Also, I had at least 2GB memory just for apache2, so it’s memory relish and it’s performance was acceptable for the security.

Things are about to change though, I’m moving my own sites to a small VPS with 512MB RAM and 1GB swap (just for sure). Why this low? Because it has to be enough. And to make this sure, I need switching to nginx + php-fpm, combined with APC from apache2 and xcache.

The host is Ubuntu 11.04 (server). Yes, it could be better, more stable, and 10.04, but I need up-to-date packages, I don’t like complying everything myself.

And the real twist: I need this to work with a WordPress 3.0 Network, with domain mapping plugin and all.

Installation

Add the nginx repository.

add-apt-repository ppa:nginx/stable
apt-get update

Install the packages we need.

Nginx, PHP, PHP-FPM, MySQL – the most needed ones

sudo apt-get install nginx-full php5-fpm php5-cli php5-dev php5-mysql php5-curl php5-gd php5-imagick php5-mcrypt php5-suhosin mysql-server

Suhosin is a security plugin, but it can conflict with lots of application. Be sure I doesn’t ruin yours.

APC

APC is avaliable via PECL, but a developement package is needed for it.

sudo apt-get install php-pear build-essential libpcre3-dev
sudo pecl install apc

Configuration

I remove the comments from the configurations files, so there are just the needs.

Nginx

/etc/nginx/nginx.conf

user www-data;
worker_processes 2;
error_log  /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
  worker_connections 1024;
  use epoll;
}

http {
  server_names_hash_bucket_size 64;
  sendfile on;
  tcp_nopush  on;
  tcp_nodelay off;
  client_max_body_size 64M;
  types_hash_max_size 8192;

  include /etc/nginx/mime.types;

  default_type text/html;

  log_format main ‘$remote_addr – $remote_user [$time_local] ‘
                ‘”$request” $status $body_bytes_sent “$http_referer” ‘
                ‘”$http_user_agent” “$http_x_forwarded_for”‘ ;

  access_log /var/log/nginx/access.log main;

  include /etc/nginx/gzip_params;

  include /etc/nginx/sites-enabled/*;
}

/etc/nginx/fastcgi_params

fastcgi_connect_timeout 60;
fastcgi_send_timeout 180;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_intercept_errors on;

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_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $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/$nginx_version;
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_param REDIRECT_STATUS 200;

/etc/nginx/gzip_params

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_http_version 1.1;
gzip_comp_level 1;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/xml+rss;

/etc/nginx/sites-available/default

server {
  listen  80;
  server_name  .domain.com;
  access_log  /var/log/nginx/domain.com.access.log;
  error_log  /var/log/nginx/domain.com.error.log;

  location / {
    root  /var/www/;
    index  index.php index.html index.htm;

    if ( $uri ~ \.(ico|gif|jpg|jpeg|png)$  ) {
      expires 30d;
    }

    # WordPress multisite files rule
    rewrite ^.*/files/(.*)$ /wp-includes/ms-files.php?file=$1 last;

    # WordPress rewrite rules
    try_files $uri $uri/ /index.php?q=$uri&$args;

  }

  location ~ \.php$ {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME /var/www$fastcgi_script_name;
  }

  location ~ /\.ht {
    deny  all;
  }

  location /nginx_status {
    stub_status on;
    access_log   off;
  }
}

The real trick is done be the rewrite rules. After hours of searching and thinking about the perfect re-write of the rewrite rules, I’ve found these solutions on a Ruby (!) forum.

PHP

/etc/php5/fpm/pool.d/www.ini

[www]
listen = 127.0.0.1:9000
listen.allowed_clients = 127.0.0.1
listen.owner = www-data
listen.group = www-data
listen.mode = 0600
user = www-data
group = www-data
pm = dynamic
pm.max_children = 4
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 512
pm.status_path = /status
php_admin_value[open_basedir] = /var/www/
php_admin_value[upload_tmp_dir] = /var/www/tmp/

/etc/php5/fpm/main.conf

pid = /var/run/php5-fpm.pid
error_log = /var/log/php5-fpm.log
log_level = notice
include=/etc/php5/fpm/pool.d/*.conf

Test and conclusion

I did not expect any noticeable difference in performance or in user experience, but I was suprised. While the load and the memory usage lower with a significant value, the speed, the actual, user-based, visitor speed gained at least 300%. And also, there’s virtually no fragmentation in APC, while with apache2… well, it used to be terrible.

Apache2: I’m going to miss you. For 10+ years I have experience with apache and apache2, but there are more important aspects than habits in the world of web servers. The need for apache3 or a really good mpm-event is getting more and more urgent. Until then, I’m going to stick with nginx.

Updates

2011.10.27. Some of the config files have been updated for better performance and eliminated redundancy.