Tran­si­tion to nginx: part 1, con­vert­ing Horde webmail

I am a long­time apache user. It may be that I touched apache 1.2 for the first time. Recent­ly I decid­ed to check out nginx. So I decid­ed to do it on my own web­mail sys­tem. The end result is, I replaced apache+mod_php with nginx+php-fpm there, and so far it does not look like I want to go back (it feels faster on direct com­pare, same serv­er, either apache or nginx start­ed to com­pare the speed, pure sub­jec­tive “mea­sure­ment”, no numbers).

The long sto­ry now.

The web­mail sys­tem uses horde. There I had apache 2.4 (pre­fork mpm) and php 5.6 via mod_php. With nginx I used php-fpm. I used the same php flags and val­ues in php_fpm like I used with mod_php. I con­fig­ured less php-fpm max-processes than I had allowed apache+mod_php to use. As nginx is not spawn­ing process­es for each con­nec­tion, I have less process­es and also less mem­o­ry allo­cat­ed as a result. Con­vert­ing the rewrite rules and mod_rewrite based black­list­ing took a while (and I have not con­vert­ed all black­lists I had before). So yes, this is not real­ly com­par­ing apples with apples (I could have tried a dif­fer­ent mpm for apache, and I could have used an fcgi based php approach instead of mod_php, I could have moved the rewrite rules out from .htac­cess files to the main config).

The result was on first login that the pages appeared notice­able faster. I direct­ly switched back to apache to con­firm. This was over a WLAN con­nec­tion. A lit­tle bit lat­er I had the same impres­sion when I test­ed this over a slow DSL link (2 MBit/s).

Here the impor­tant parts of the apache config:

ExpiresActive On
ExpiresDefault "now plus 3 hours"
ExpiresByType image/* "now plus 2 months"
ExpiresByType text/javascript "now plus 2 months"
ExpiresByType application/x-javascript "now plus 2 months"
ExpiresByType text/css "now plus 2 months"

SetEnvIfNoCase Request_URI "\.gif$" cache_me=1 SetEnvIfNoCase Request_URI "\.png$" cache_me=1 SetEnvIfNoCase Request_URI "\.jpg$" cache_me=1 SetEnvIfNoCase Request_URI "\.jpeg$" cache_me=1 SetEnvIfNoCase Request_URI "\.ico$" cache_me=1 SetEnvIfNoCase Request_URI "\.css$" cache_me=1

# Allow caching on media files
<IfModule mod_headers.c>
Header merge Cache-Control "public" env=cache_me
  <IfModule ssl_module.c>
    Header add Strict-Transport-Security "max-age=15768000"
  </IfModule>
</IfModule>
<Location />
# Insert filter
SetOutputFilter DEFLATE

# Netscape 4.x has some problems... BrowserMatch ^Mozilla/4 gzip-only-text/html

# Netscape 4.06-4.08 have some more problems BrowserMatch ^Mozilla/4\.0[678] no-gzip

# MSIE masquerades as Netscape, but it is fine BrowserMatch \bMSIE !no-gzip !gzip-only-text/html # Don't compress images SetEnvIfNoCase Request_URI \ \.(?:gif|jpe?g|png|gz|bz2|zip)$ no-gzip dont-vary

# Make sure proxies don't deliver the wrong content Header append Vary User-Agent env=!dont-vary </Location>

SetEnvIfNoCase Request_URI "\.js$" cache_me=1
Alias /Microsoft-Server-ActiveSync /usr/local/www/horde/rpc.php
RedirectPermanent /.well-known/carddav /horde/rpc.php
AcceptPathInfo on

And here most parts of my nginx.conf suit­able for Horde, includ­ing the rewrite rules from .htac­cess files:

worker_processes  1;

error_log  <file>;

events {         worker_connections      1024;         use                     kqueue; }

http {         include    mime.types;         default_type  application/octet-stream;

        access_log  <file>;

        sendfile        on;         keepalive_timeout       15;         client_body_timeout     300;         client_header_timeout   12;         send_timeout        300;         client_body_in_file_only clean;         client_body_buffer_size 128k;         client_max_body_size 10M;

        gzip on;         gzip_min_length 1000;         gzip_types       text/plain text/xml text/css application/xml application/xhtml+xml application/rss+xml application/javascript application/x-javascript;         gzip_disable     "msie6";

        include blacklist.conf;

        server {                 listen     443 ssl spdy;                 server_name  <hostname>;

                ssl_certificate         <file>;                 ssl_certificate_key     <file>;

                ssl_session_cache       shared:SSL:10m;                 ssl_session_timeout     15m;

                ssl_ciphers     <ciper_list>;
                ssl_prefer_server_ciphers  on;

                # optional: see https://www.owasp.org/index.php/List_of_useful_HTTP_headers                 add_header                        strict-transport-security "max-age=31536000";                 add_header                        x-frame-options          "sameorigin";                 add_header                        x-xss-protection        "1; mode=block";                 add_header                        x-content-type-options        "nosniff";

                root                            /usr/local/www/horde;                 index                              index.php;

                charset utf8;

                access_log  <logfile>;

                if ($bad_client) { return 403; }

                location / {                         location /Microsoft-Server-ActiveSync {                                 alias                   /usr/local/www/horde/rpc.php;                                 fastcgi_split_path_info ^(.+\.php)(/.+)$;                                 fastcgi_keep_conn       on;                                 include                 fastcgi_params;                                 fastcgi_param           SCRIPT_FILENAME /usr/local/www/horde/rpc.php;                                 fastcgi_pass            unix:/var/run/php.fcgi;                         }

                        location /autodiscover/autodiscover.xml {                                 alias                   /usr/local/www/horde/rpc.php;                                 fastcgi_split_path_info ^(.+\.php)(/.+)$;                                 fastcgi_keep_conn       on;                                 include                 fastcgi_params;                                 fastcgi_param           SCRIPT_FILENAME /usr/local/www/horde/rpc.php;                                 fastcgi_pass            unix:/var/run/php.fcgi;                         }

                        location /Autodiscover/Autodiscover.xml {                                 alias                   /usr/local/www/horde/rpc.php;                                 fastcgi_split_path_info ^(.+\.php)(/.+)$;                                 fastcgi_keep_conn       on;                                 include                 fastcgi_params;                                 fastcgi_param           SCRIPT_FILENAME /usr/local/www/horde/rpc.php;                                 fastcgi_pass            unix:/var/run/php.fcgi;                         }

                        location ^~ /(static|themes)/ {                                 expires                 1w;                                 add_header              Cache-Control public;                         }

                        location ^~ /services/ajax.php {
                                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                                fastcgi_keep_conn       on;
                                include                 fastcgi_params;
                                fastcgi_param           SCRIPT_FILENAME $document_root$fastcgi_script_name;
                                fastcgi_pass            unix:/var/run/php.fcgi;
                        }

                        location ~ \.php {                                 # Zero-day exploit defense.                                 # http://forum.nginx.org/read.php?2,88845,page=3                                 # Won't work properly (404 error) if the file is not stored on this server, which is entirely possible with php-fpm/php-fcgi.                                 # Comment the 'try_files' line out if you set up php-fpm/php-fcgi on another machine.  And then cross your fingers that you won't get hacked.                                 try_files $uri =404;

                                fastcgi_split_path_info ^(.+\.php)(/.+)$;                                 fastcgi_keep_conn       on;                                 fastcgi_index           index.php;                                 include                 fastcgi_params;                                 fastcgi_param           SCRIPT_FILENAME $document_root$fastcgi_script_name;                                 fastcgi_pass            unix:/var/run/php.fcgi;                         }

                        try_files                          $uri $uri/ /rampage.php?$args;                 }

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
                root   /usr/local/www/nginx-dist;
        }

        # deny access to .htaccess files, if Apache's document root         # concurs with nginx's one         #                 location ~ /\.ht {                         deny all;                 }                 location ~ /(config|lib|locale|po|scripts|templates)/ {                         deny all;                 }                 location ^~ /rpc/ {                         if (!-e $request_filename){                                 rewrite ^(.*)$ /rpc/index.php/$1 break;                         }                 }                 location ^~ /kronolith/feed/ {                         if (!-e $request_filename){                                 rewrite ^(.*)$ /kronolith/feed/index.php?c=$1 break;                         }                 }

                location ^~ /content/ {
                        if (!-e $request_filename){
                                rewrite ^(.*)$ /content/index.php break;
                        }
                }
                location ^~ /whups/(queue|query)/ {
                        if (!-e $request_filename){
                                rewrite ^/([0-9]+)/?$ /whups/queue/index.php?id=$1;
                        }
                        rewrite ^/([0-9]+)/rss/?$ /whups/queue/rss.php?id=$1;
                        rewrite ^/([a-zA-Z0-9_]+)/?$ /whups/queue/index.php?slug=$1;
                        rewrite ^/([a-zA-Z0-9_]+)/rss/?$ /whups/queue/rss.php?slug=$1;
                }
                location ^~ /whups/ticket/ {
                        if (!-e $request_filename){
                                rewrite ^/([0-9]+)/?$ /whups/ticket/index.php?id=$1;
                        }
                        rewrite ^/([0-9]+)/rss/?$ /whups/ticket/rss.php?id=$1;
                        rewrite ^/([0-9]+)/([a-z]+)(\.php)?$ /whups/ticket/$2.php?id=$1 break;
                }
                location ^~ /.well-known/carddav {
                        return 301 https://webmail.Leidinger.net/rpc.php;
                }
                location ^~ /admin/ {
                        allow <local>;
                        deny all;
                }
                location ~ /test.php$ {
                        allow <local>;
                        deny all;
                }

                # Media: images, icons, video, audio, HTC, archives                 location ~* \.(?:jpe?g|gif|png|ico|cur|gz|bz2|tbz|tgz|svg|svgz|mp4|ogg|ogv|webm|htc|css|js|pdf|zip|rar|tar|txt|pl|conf)$ {                         try_files $uri =404;

                        expires 1w;                         access_log off;                         add_header Cache-Control "public";                 }         } }

2 thoughts on “Tran­si­tion to nginx: part 1, con­vert­ing Horde webmail”

  1. hi! i have one ques­tion, do you have the direc­tive cgi.path_info enabled at php.ini?
    many thanks in advanced

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.