Afternoon.

So I've been getting stuck into making Drupal 4.7 (and 5.0) work with Nginx, which is a bit like Lighttpd except without the firehose-esque memory leaks you get with Lighty and actual web traffic busier than a trickle.

This has worked for me for the last several days on (! NSFW !) cliterati.co.uk, which roars through about 30 HTTP requests per second.

Credit: This page was completely invaluable, and everything Drupal-ish here is merely minor edits to that earlier work.

Why would you want to do this?

Because if you're running Apache/Apache2 with mod_php on a dedicated server with 1Gb of memory, and you have a lot of traffic, and more than about 50 of your visitors are logged in and posting to forum.module most of the time, then your dedicated server can't run Drupal. This is nuts. While people are raging against the non-existent caching for uid>0 in Drupal, you may want to cut your static memory requirement by about 85% at a stroke by showing Apache the door.

Nginx (and lighttpd) do more than just this, in performance terms, but even if they didn't I'd still need to run one of them to keep such a server afloat.

Nginx

Firstly you have to install nginx, which is not going to be covered here. I went for compile-from-source because the versions in Debian repositories are ancient. Next, here's the configuration details I've found to work with Drupal and URL aliasing. I'm posting an example of an entire http{} section from the config file. It passes everything PHP-related to one or more PHP fastcgi processes listening on port 8888, which you'll be setting up after this.

In nginx.conf:

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

    #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  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  20;
    tcp_nodelay        on;

    #gzip  on;

    server {
        listen       192.168.0.1:80;  # Replace this IP and port with the right ones for your requirements
        server_name  example.com www.example.com;  # Multiple hostnames seperated by spaces.  Replace these as well.

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location = / {
            root   /path/to/drupal;  # Again, replace this.  
            index  index.php;
        }

        location / {
            root   /path/to/drupal;
            index  index.php index.html;

            if (!-f $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

            if (!-d $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

        }

        error_page  404              /index.php;

        # serve static files directly
        location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico)$ {
              access_log        off;
            expires           30d;
        }

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

        location ~ .php$ {
          fastcgi_pass   127.0.0.1:8888;  # By all means use a different server for the fcgi processes if you need to
          fastcgi_index  index.php;

          fastcgi_param  SCRIPT_FILENAME  /path/to/drupal$fastcgi_script_name;   # !! <--- Another path reference for you.
          fastcgi_param  QUERY_STRING     $query_string;
          fastcgi_param  REQUEST_METHOD   $request_method;
          fastcgi_param  CONTENT_TYPE     $content_type;
          fastcgi_param  CONTENT_LENGTH   $content_length;
        }



        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}

     }
}

PHP and Fastcgi

I'm not mentioning PHP 4 or 5 because this part is exactly the same for each. I'm referring to 'php5-cgi' because that's the name of the right binary for the right PHP version on my Debian-based server. Your mileage may vary.

This shell script will launch a few fastcgi PHP processes bound to port 8888 for Nginx to talk to. I launch it as root - it starts the processes as the Debian apache user and exits.

#!/bin/bash

## ABSOLUTE path to the PHP binary
PHPFCGI="/usr/bin/php5-cgi"

## tcp-port to bind on
FCGIPORT="8888"

## IP to bind on
FCGIADDR="127.0.0.1"

## number of PHP children to spawn
PHP_FCGI_CHILDREN=5

## number of request before php-process will be restarted
PHP_FCGI_MAX_REQUESTS=1000

# allowed environment variables sperated by spaces
ALLOWED_ENV="ORACLE_HOME PATH USER"

## if this script is run as root switch to the following user
USERID=www-data

################## no config below this line

if test x$PHP_FCGI_CHILDREN = x; then
  PHP_FCGI_CHILDREN=5
fi

ALLOWED_ENV="$ALLOWED_ENV PHP_FCGI_CHILDREN"
ALLOWED_ENV="$ALLOWED_ENV PHP_FCGI_MAX_REQUESTS"
ALLOWED_ENV="$ALLOWED_ENV FCGI_WEB_SERVER_ADDRS"

if test x$UID = x0; then
  EX="/bin/su -m -c \"$PHPFCGI -q -b $FCGIADDR:$FCGIPORT\" $USERID"
else
  EX="$PHPFCGI -b $FCGIADDR:$FCGIPORT"
fi

echo $EX

# copy the allowed environment variables
E=

for i in $ALLOWED_ENV; do
  E="$E $i=${!i}"
done

# clean environment and set up a new one
nohup env - $E sh -c "$EX" &> /dev/null &

Initial results

I've moved two phpAdsNew ad servers, a fairly-busy Wordpress blog, the above-mentioned Drupal site and my own Drupal site from Apache2 into Nginx in the last week. Processor use has increased on the server, but critically the ongoing web+database memory use has come down by over 300MB. This means that the box hasn't gone into swap for a week (normally it was paging out every day), server response times are up and (because I'm also using APC to store Drupal's source files in memory) page download speeds are also up considerably.

Basically, it's saved a server.

I've tested it with Drupal 4.7 and Drupal 5.0, and with PHP4.3.3 and PHP5.2.0. All those configurations work.

Caveats

Drupal can serve static files either directly or via Drupal itself - it's a configuration option (in Drupal 4.7 it's in admin/settings - I don't yet have 5.0 seared into memory). This configuration requires that you serve those files directly. If Drupal's configured the other way your site will look incredibly odd, and image.module in particular breaks horribly. Me, I don't care. If this is an issue for you, it might be time to get into that in the comments.

Enjoy... and if you've got suggestions to improve this, I'd love to hear them.

Comments

nixuzer’s picture

handelaar,

Thanks for putting this information out there. We're looking at getting our own server and consolidating all of our Drupal stuff onto it as well as some misc traffic. Would you mind providing the hardware configuration of the server (RAM, disk(s), CPU(s), etc) as well as the type of traffic you get (hits per day/week/month whatever)? We're trying to figure out if a Celeron would do us to start or do we need something higher powered and if so how much more do we need.

Your time putting this together is appreciated.

handelaar’s picture

It's a dedicated box at Servermatrix. Intel(R) Celeron(R) CPU 2.40GHz, 1Gb RAM, 80Gb HDD.

Like I said, it's doing 30-odd HTTP requests per second. The majority of the traffic at any given moment is coming from one of two sites, which between them serve 1.3m pages per month. A bunch of other co-hosted Drupal installs bring that total over 2m between them.

Interestingly, I found that locking MySQL down a little (using my-medium.cnf) and restricting its memory footprint works rather better than giving it my-huge.cnf and running the risk of sending the whole box into swap during peak periods. And both the high-traffic sites have had their databases converted to InnoDB to avoid the problem of one posted comment causing the site to be unable to show any pages to anyone for several seconds, because 400 SELECT queries are stalled for every 1 second spent on that INSERT (and the time spent waiting for table locks before the INSERT can begin).

"MyISAM considered harmful", indeed.

handelaar’s picture

Right under this page on a Google query for nginx rewrite examples, Scott Yang draws my attention to the part of the nginx docs which I couldn't see staring me in the face.

Where Apache uses !-f then !-d in sequence, I was looking for a way to ape it. Turns out I didn't need to. So above where I wrote this:

            if (!-f $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

            if (!-d $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

...you should instead use this...

            if (!-e $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

...because Nginx has the "-e" directive which matches against a file, directory or symlink all at once, rendering my attempts to replicate the Apache way unnecessary.

furmans’s picture

Try this instead:

            if (!-e $request_filename) {
                rewrite  ^/(.*)$  /index.php?q=$1  last;
                break;
            }

Drupal is expecting something of the form "node/1" and not "/node/1" after the "q=". There is at least one module, GlobalRedirect, that does not tolerate the leading "/". The rule above fixes the problem.

Current Drupal site: Inventor Spot

vwX’s picture

I haven't used drupal under nginx in a subdir, until the other day. Found out the above won't work. You have to do something like:

 if ($request_uri ~* ^.*/.*$) {
     rewrite ^/(\w*)/(.*)$ /$1/index.php?q=$2 last;
     break;
}

for your rewrite instead.

For example, if your subdirectory is called mysub a request of http://example.com/mysub/admin/report/status will be rewritten correctly as http://example.com/mysub/index.php?q=admin/report/status instead of http://example.com/mysub/admin/report/index.php?q=status. Remember that .* is greedy.

There is probably a better way to write this, but this works for me for now.

Have fun and check my Drupal Profile: http://drupal.org/user/519

Anonymous’s picture

So true, it is also failing with multi-lingual setups if you don't do this...

nixuzer’s picture

handelaar, thanks for the information on your server. Based on the fact that most of our sites will be running semi-static content (i.e. usually updated by us and not the users) I think we'll be okay with a lower end box.

As an aside I ran some performance numbers using http_load and the difference between Apache2 and nginx was pretty dramatic. Granted this isn't completely an apples to apples comparison but I did do some tweaking with both to get the numbers more inline but nothing too drastic. This was on a fresh Debian Stable install using a 'as delivered' from Dell Precision 360 w/1GB of RAM and a 3GHz Intel P4 inside my local network where both machines were connected to a Linksys WRT54G. Also I repeated this test several times to verify the results were consistent. The Drupal install was the latest 4.7.X branch with 7 pages set up and referenced in the urls.txt file.

Server: Apache2
Apache2 caching=On
Drupal Caching=On
$ http_load.exe -parallel 50 -seconds 30 urls.txt
547 fetches, 50 max parallel, 3.62053e+06 bytes, in 30.015 seconds
6618.88 mean bytes/connection
18.2242 fetches/sec, 120624 bytes/sec
msecs/connect: 122.124 mean, 3250 max, 0 min
msecs/first-response: 2337.75 mean, 5016 max, 141 min
HTTP response codes:
code 200 -- 547

Server: nginx
PHP_FCGI_CHILDREN=6
PHP_FCGI_MAX_REQUESTS=250
eAccelerator=On
Drupal Caching=On
Browser Load Time = 1
$ http_load.exe -parallel 50 -seconds 30 urls.txt
2748 fetches, 50 max parallel, 1.83063e+07 bytes, in 30.015 seconds
6661.68 mean bytes/connection
91.5542 fetches/sec, 609905 bytes/sec
msecs/connect: 6.52766 mean, 3265 max, 0 min
msecs/first-response: 215.662 mean, 3625 max, 0 min
HTTP response codes:
code 200 -- 2748

I'm still playing around so I'm sure the numbers from nginx will only get better.

beryl’s picture

I dont think that this would be a fair comparison, we all know that eAccelerator makes apache 2 very much faster, yet you didnt test apache2 with it here, but tested nginx with it.

vwX’s picture

I added

location ~* /(modules|themes|misc|sites|profiles|scripts)/ {
                        return 404;
                }

to prevent download of the non .php extension files.

Have fun and check my Drupal Profile: http://drupal.org/user/519

nixuzer’s picture

Are you sure about the themes directory?

To test this make sure to restart nginx and clear your browser cache.

vwX’s picture

I'll blame lack of sleep. This doesn't actually work, didn't realize how many .css files are under the modules dir now. Here is the better config.

               location ~* /(modules|themes|scripts|sites)/ {
                        if (-f $request_filename) {
                                rewrite \.(module|inc|info|engine|sql|sh)$  / permanent;
                        }
                }

Have fun and check my Drupal Profile: http://drupal.org/user/519

snelson’s picture

My /admin/build/modules page wouldn't submit after using this bit. Why not just copy how .htaccess does it?

This worked nicely for me:

  # hide protected files
  location ~* \.(engine|inc|info|install|module|profile|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(code-style\.pl|Entries.*|Repository|Root|Tag|Template)$ {
    deny all;
  }

--
Scott Nelson
This by Them, LLC
Services Module

nezroy’s picture

Here's a Debian-style init script for starting and stopping the PHP FCGI instances. The one thing it doesn't do is clear the environment like the original does. You could probably interleave env into this somehow to get it to do that, but I didn't go that far on this first pass. Note that, as with any use of start-stop-daemon and --background, you don't get much error checking on startup.

#!/bin/sh -e

# Start or stop PHP FastCGI handlers
#
# based on Postfix's init.d script

SSD=/sbin/start-stop-daemon
DAEMON=/usr/bin/php5-cgi
NAME=phpcgi
FCGIPORT="8080"
FCGIADDR="127.0.0.1"
export PHP_FCGI_CHILDREN=5
export PHP_FCGI_MAX_REQUESTS=1000
USERID=www-data
PIDFILE=/var/run/fastcgi.pid
TZ=
unset TZ

test -x $DAEMON || exit 0

. /lib/lsb/init-functions
DISTRO=$(lsb_release -is 2>/dev/null || echo Debian)

running() {
    if [ -f ${PIDFILE} ]; then
        pid=$(sed 's/ //g' ${PIDFILE})
        exe=$(ls -l /proc/$pid/exe 2>/dev/null | sed 's/.* //;')
        if [ "X$exe" = "X$DAEMON" ]; then
            echo y
        fi
    fi
}
case "$1" in
    start)
        log_daemon_msg "Starting PHP FastCGI Handler" ${NAME}
        RUNNING=$(running)
        if [ -n "$RUNNING" ]; then
            log_end_msg 0
        else
        if ${SSD} --start --chuid ${USERID} --background --pidfile ${PIDFILE} --make-pidfile --startas ${DAEMON} -- -q -b ${FCGIADDR}:${FCGIPORT}; then
                log_end_msg 0
            else
                log_end_msg 1
            fi
        fi
    ;;

    stop)
        RUNNING=$(running)
        log_daemon_msg "Stopping PHP FastCGI Handler" ${NAME}
        if [ -n "$RUNNING" ]; then
        if ${SSD} --stop --pidfile ${PIDFILE} --signal 15; then
                rm -f ${PIDFILE}
                log_end_msg 0
            else
                log_end_msg 1
            fi
        else
            log_end_msg 0
        fi
    ;;

    restart)
        $0 stop
        $0 start
    ;;

    force-reload|reload)
        $0 stop
        $0 start
    ;;

    *)
        log_action_msg "Usage: /etc/init.d/fastcgi {start|stop|restart|reload|force-reload}"
        exit 1
    ;;
esac

exit 0
jordi’s picture

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=426780 has yet another init script that is being proposed for inclussion in Debian's php5-cgi.

furmans’s picture

If you're using the imagecache module, serving static files directly like the original poster does, nginx bypasses the image rescaler. In order to make image cache work with the following,

# serve static files directly
        location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico)$ {
              access_log        off;
            expires           30d;
        }

you need to add the code below to force the imagecache images through PHP. My code assumes that your imagecache image dir is set up under /files/imagecache.

        # imagecache needs to have php read any files that it's planning to manipulate                                                  
        location ^~ /files/imagecache/ {
            index  index.php index.html;

            # assume a clean URL is requested, and rewrite to index.php                                                                 
            if (!-e $request_filename) {
		rewrite  ^/(.*)$  /index.php?q=$1  last;
                break;
            }
        }

Current Drupal site: Inventor Spot

ldway’s picture

I am not able to get the rewrite working correctly...

Without the rewrite rule all works as expected. (No rewrite but the main site shows, just no links work because of no rewrite)

When I have the rewrite section in my conf file, I get no CSS, pictures, just straight text with a darn funny looking white page.

Here is what I have in my conf file:

===============================================
# serve static files directly
location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico)$ {
access_log off;
expires 30d;
break;
}

if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?q=$1 last;
break;
}

error_page 404 /index.php;

location ~ \.php$ {
include /usr/local/nginx/fastcgi.conf;
fastcgi_pass 127.0.0.1:57102;
fastcgi_index index.php;
}
===============================================

Very strange indeed as it seems like the above would work. When I comment out the rewrite section the home page displays fine but just not rewrite (as expected).... As soon as I put that rewrite section back in I get the page with straight text, no css formatting, no pics, etc.

ANYONE have any ideas? Spend all day on trying to figure this out and I am completely stumped.

mjrich’s picture

Did you ever figure this one out ?

It's currently stumping me also (doubly so, as I've set this up several times in the past without problem -- this time, for some unknown reason it won't go away).

NickSI’s picture

You have typo in your config. Replace

location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico)$ {....

with (note the "\" before second ".")

location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico)$ {
mjrich’s picture

True, but that doesn't seem to be the (only) problem.

There must be some other curiosity that is putting a spanner in the works.

EDIT: Right, after much gnashing of teeth I have it working. I commented out the section for serving static files (location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico)$ { ... ), re-enabled the clean urls section (if (!-e $request_filename) {rewrite ^/(.*)$ /index.php?q=$1 last; break;}), and re-enabled clean urls within Drupal (6.14, bare install) again. Voila !

madra’s picture

ldway: When I have the rewrite section in my conf file, I get no CSS, pictures, just straight text with a darn funny looking white page.

mjrich: Did you ever figure this one out ?

i've just run into a similar problem myself; everything seems to be working OK, apart from my theme, which is completely messed up. after a lot of hair-tearing out, i happened to have a look inside one of my theme's CSS files and found the answer - i think!

my theme uses PHP within some of the CSS files for defining colours/dimensions at the top of the file [for ease of making global changes]. under apache, PHP was set to handle these CSS files by means of the following .htaccess file in the theme directory:

AddHandler application/x-httpd-php .css
php_value default_mimetype "text/css"

obviously the PHP inside these CSS files is not being parsed in the new nginx setup and the same would be true for anyone else who's using conditional PHP statements inside their theme's CSS files.

the answer would seem to be to tell nginx to also send any CSS files it finds within /sites/<sitename>/themes/<themename> to php-fastcgi for processing, but unfortunately 99% of regex is 'alphabetti spaghetti' to me, so i'm not sure how i'd construct such a directive in the correct syntax.

any ideas, anyone?

mpapet’s picture

The shell script as shown above did not work for me. (Debian Etch circa 5/2008). I had two issues:

1. It did not spawn anything.
2. When I tweaked it some more, I got it to spawn a parent, but no chldren.
3. When I finally got it to spawn, there is another issue where the user account that started the script (not www-data) owned the process and threw permission errors.

So, I revised the punchline of the script a bit. Instead of:

if test x$UID = x0; then
EX="/bin/su -m -c \"$PHPFCGI -q -b $FCGIADDR:$FCGIPORT\" $USERID"
else
EX="$PHPFCGI -b $FCGIADDR:$FCGIPORT"
fi

I used sudo with as few modifications to /etc/sudoers as possible. If you spent some time modifying /etc/sudoers, you could probably come up with a more elegant approach:

if test x$UID = x0; then
EX="sudo -u www-data env PHP_FCGI_CHILDREN=4 $PHPFCGI -c $PHP_CONFIG_FILE -q $
else
echo "Sorry, can't start. Must start as root"
fi

Pay careful attention to the "env..." bit. This allows the php5-cgi to make children. To run multiple versions of php, I added the $PHP_CONFIG_FILE stanza as follows:

##DIRECTORY to *find* the php.ini
PHP_CONFIG_FILE="/etc/php5/cgi"

I still have a problem where the listener dies with peak loads. php5-cgi sucks all of the cpu up and then dies. I don't care about the cpu running near 100%. it seems like it can't queue very well. Does anyone have any experience with nginx instance as load balancer to multiple php5-cgi listeners?

westbywest’s picture

There is pending/orphaned Debian bug report with a more canonical implementation of the fastcgi startup script.

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=426780

NickSI’s picture

In initital configuraion:

        # serve static files directly
        location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico)$ {
              access_log        off;
            expires           30d;
        }

this statement breaks Drupal AJAX upload. You shoud add backslash to the secon "." in regular expression to fix it. Updated statement should look like

        # serve static files directly
        location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico)$ {
              access_log        off;
            expires           30d;
        }
Setzler’s picture

I love you.

dgrant’s picture

How do I block access to a specific directory? like /var/www/drupal/sites/www.willmusic.ca/files/backup_migrate. There is a .htaccess file in that directory that does a "deny all"...

NickSI’s picture

nginx does not use .htaccess so you have to specify access rules in nginx config file.

        # fix for backup_migrate
        location ~* ^/files/backup_migrate {
          deny all;
        }
Dzuma’s picture

I have this .htaccess, and I would like to change it to rewrite.
Can somebody please, give me an example using one of these rules below, how I would do it?
This .htaccess is located at: /var/mysql/www/www.yuwa.org/bioskop
Thank you.

RewriteRule ^category/([0-9]+)/.*\.p([0-9]+)\.(htm|html)$ listplaylists.php?cat=$1&p=$2 [QSA,L]
RewriteRule ^category/([0-9]+)/.*\.(htm|html)$ listplaylists.php?cat=$1 [QSA,L]
RewriteRule ^playlists/([0-9]+)/.*\.p([0-9]+)\.(htm|html)$ listplaylists.php?uid=$1&p=$2 [QSA,L]
RewriteRule ^playlists/([0-9]+)/.*\.(htm|html)$ listplaylists.php?uid=$1 [QSA,L]
RewriteRule ^videos/([0-9]+)/.*\.p([0-9]+)\.(htm|html)$ listvideos.php?uid=$1&p=$2 [QSA,L]
RewriteRule ^videos/([0-9]+)/.*\.(htm|html)$ listvideos.php?uid=$1 [QSA,L]
RewriteRule ^playlist/([0-9]+)/.*\.p([0-9]+)\.(htm|html)$ listvideos.php?pid=$1&p=$2 [QSA,L]
RewriteRule ^playlist/([0-9]+)/.*\.(htm|html)$ listvideos.php?pid=$1 [QSA,L]
RewriteRule ^tag/(.*)\.p([0-9]+)$ listvideos.php?tag=$1&p=$2 [QSA,L]
RewriteRule ^tag/(.*)$ listvideos.php?tag=$1 [QSA,L]
RewriteRule ^video/([0-9]+)/.*\.(htm|html)$ showvideo.php?id=$1 [QSA,L]
RewriteRule ^member/([0-9]+)/.*\.(htm|html)$ member/user.php?id=$1 [QSA,L]
RewriteRule ^playlists(\.p([0-9]+))?\.(htm|html)$ listplaylists.php?p=$2 [QSA,L]
RewriteRule ^videoslist(\.p([0-9]+))?\.(htm|html)$ listvideos.php?p=$2 [QSA,L]
RewriteRule ^memberslist(\.p([0-9]+))?\.(htm|html)$ listusers.php?p=$2 [QSA,L]
RewriteRule ^index\.(htm|html)$ index.php [QSA,L]
Cronjobs’s picture

Best documentation is in russian, but you can try http://wiki.codemongers.com/NginxConfiguration

yookoala’s picture

The "serve static files directly" config doesn't work when you have multiple virtualhost in Nginx.
It seems you have to define "root" directive there.

After referencing some comments here I changed the configuration.
The below configuration works well on my testing server:

        # serve static files directly
        location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico)$ {
              root   /path/to/drupal;
              access_log        off;
              expires           30d;
              break;
        }

Or alternatively, you can add the "root" directive to "server" like this":

    server {
        listen       192.168.0.1:80;               # Replace this IP and port with the right ones for your requirements
        server_name  example.com www.example.com;  # Multiple hostnames seperated by spaces.  Replace these as well.
        root   /path/to/drupal;                    # ***** Replace this  *****

        #charset koi8-r;
        #access_log  logs/host.access.log  main;

        location = / {
            index  index.php;
        }

        location / {
            index  index.php index.html;

            if (!-f $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

            if (!-d $request_filename) {
                rewrite  ^(.*)$  /index.php?q=$1  last;
                break;
            }

        }

        error_page  404              /index.php;

        # serve static files directly
        location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico)$ {
            access_log        off;
            expires              30d;
            break;
        }

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

        location ~ .php$ {
          fastcgi_pass   127.0.0.1:8888;  # By all means use a different server for the fcgi processes if you need to
          fastcgi_index  index.php;

          fastcgi_param  SCRIPT_FILENAME  /path/to/drupal$fastcgi_script_name;   # ***** Replace this  *****
          fastcgi_param  QUERY_STRING     $query_string;
          fastcgi_param  REQUEST_METHOD   $request_method;
          fastcgi_param  CONTENT_TYPE     $content_type;
          fastcgi_param  CONTENT_LENGTH   $content_length;
        }



        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}

     }
StefanScottAlexx’s picture

Hi -

I installed nginx + PHP-FPM, and set up multiple Drupal sites using the "more secure" directory layout following the instructions from Justin Hileman:

http://justinhileman.info/blog/2007/06/a-more-secure-drupal-multisite-in...

So far I've only configured site1.com to use Drupal, and site2.com and site3.com display "Welcome to Nginx" for now, which is fine. I configured my nginx.conf file based on the examples above, and also here:

http://www.codegobbler.com/drupal-nginx-fastcgi-setup-and-configuration

It seems to be working ok so far - I've set up a database, installed some modules and themes, and created a few pages.

Now as an additional test, I tried pointing my browser at my numeric IP address, eg:

http://208.77.188.166/ ## this is the official IP address of example.com !!

I hoped it would go by default to the first site in my nginx.conf file (site1.com) - but instead it goes to my Drupal installation page.

I should clarify that I've already set up site1.com in Drupal - so the installation page no longer appears when I go to http://site1.com - it only appears when I go to http://208.77.188.166 - because Drupal is somehow grabbing that IP address and redirecting it to an installation page.

I don't want to expose this Drupal installation page to the public. It's not a big security risk I think, because the page shows errors saying ./sites/default/default.settings.php and sites/default/files don't exist yet and need their permissions to be set. But still I'd like to prevent this Drupal installation page from being displayed in case anybody happens to point a browser at my IP address!

How do I change my ningx.conf file so that entering a numeric IP address in my browser will go to the first site in my nginx.conf file (site1.com)?

Here's my nginx.conf file:

user  www-data www-data;
worker_processes  2;

error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

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

    #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  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    gzip on;
    gzip_comp_level 1; gzip_proxied any;
    gzip_types text/plain
               text/html
               text/css
               application/x-javascript
               text/xml
               application/xml
               application/xml+rss
               text/javascript;

    server {
        listen       80;
        server_name  .site1.com;

        location / {
            root   /usr/local/nginx/html/site1;
            index  index.php;
        }

        location / {
            root   /usr/local/nginx/html/site1;
            index  index.php index.html;

            if (!-e $request_filename) {
                rewrite  ^/(.*)$  /index.php?q=$1  last;
                break;
            }
        }

        # hide protected files
        location ~* \.(engine|inc|info|install|module|profile|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(code-style\.pl|Entries.*|Repository|Root|Tag|Template)$ {
            deny all;
        }

        # hide backup_migrate files
        location ~* ^/files/backup_migrate {
            deny all;
        }

        # serve static files directly
        location ~* ^.+\.(jpg|jpeg|gif|css|png|js|ico)$ {
            root /usr/local/nginx/html/site1;
            access_log        off;
            expires           30d;
            break;
        }

        error_page  404              /index.php;

        error_page   500 502 503 504  /50x.html;

        location = /50x.html {
            root   html;
        }

        location ~ \.php$ {
            root   html/site1;
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            include /usr/local/nginx/conf/fastcgi_params;
        }
    }

    server {
        listen       80;
        server_name  .site2.com;

        location / {
            root   html/site2;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
    server {
        listen       80;
        server_name  .site3.com;

        location / {
            root   html/site3;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

Thanks for any help!

StefanScottAlexx’s picture

OK, fixed it - but 'listen ... default' wasn't working for some reason - somehow Drupal was grabbing it anyways and redirecting to the "Install Drupal" page.

Instead I followed the advice here:

https://calomel.org/nginx.html

and added this:

## Deny access to any host other than (www.)mydomain.com
server {
server_name _; #default
return 444;
}

BEFORE all the other server directives in the nginx.file - and it worked!

Thanks!

edrex’s picture

you should perhaps also make sites/default a symlink to sites/site1.com

meramo’s picture

Any thoughts of Nginx+Imagecache without Apache config?

I cant get it work together and havent found solution yet, if someone can help, please do it, cause the thing is crucial.

mrfelton’s picture

try accessing http://your-site/som-php-page-that-doesnt-exist.php. do you get the Drupal 404? or a plaintext message saying "No input file specified" ?

If if have the php redirect in the server{} section, Drupal handles the 404 pages, but then other location directives do not work correctly. If I have it in a location / {} or location = / {} other location directives work correctly, but I get the plain text error when accessing non existent .php files.

(by 'the php redirect' I mean:)

if (!-d $request_filename) {
    rewrite  ^(.*)$  /index.php?q=$1  last;
    break;
}

--
Tom
www.systemseed.com - drupal development. drupal training. drupal support.

brianmercer’s picture

NT

seanberto’s picture

Anyone have any issues getting NginX working with outgoing email? Drupal's not sending out user registration emails, nor contact form submissions. The Drupal logs show that emails going out correctly, but it's not being received.

Sean Larkin

mshaver’s picture

Problem I'm having with my nginx and drupal multisite setup?

The problem is with the password reset function in Drupal. Everything works as expected, until you get to the point where you are selecting the "login" button that is supposed to redirect you to your account edit page where you change your password. In my case it instead does not redirect and gives you an error for trying to use the reset url again. I read someplace that running PHP in CGI mode sometimes has issues with sessions after a redirect in the URL?

Does this sound familiar at all?

axel’s picture

The same problem on Drupal 6.2 and Nginx 0.8.34. Lost sessions on password recovery.

omega8cc’s picture

Everyone looking for latest, working and supported configuration examples, tips and tricks, please join our Nginx group:

http://groups.drupal.org/nginx

~Grace -- Turnkey Drupal Hosting on Steroids -- http://omega8.cc

Mac Ryan’s picture

...the group does not seem to have (yet) much support offered in its threads.

As this thread has quite a number of modifications / corrections / updates to the original configuration file, may I suggest somebody prepare a bit of how-to for the abovementioned group? That would be very helpful! :)

[I can't do that myself as I still did not manage to make drupal/nginx tandem to properly work...]

Thanks!

omega8cc’s picture

There is fully working example available, now with help for custom nginx + php-fpm build, and it works with any Drupal site, not only with Aegir or Boost enabled:

http://groups.drupal.org/node/26363

~Grace -- Drupal on Steroids -- http://omega8.cc

NeoID’s picture

I followed the examples mentioned here in order to setup nginx for Drupal, however... if I turn on site-compression, I get some binary junk outputted...
Anyone knows what I may have done wrong or forgotten?

omega8cc’s picture

You should turn it off in Drupal, since with compression enabled in Nginx you are trying to do it twice.

Slovak’s picture

Should I assume that

expires 30d;

should only be present for static files, as Drupal handles its own caching? Or would it be of benefit for nginx to serve all cached pages and turn that function off in Drupal?

brianmercer’s picture

Yes, only for static resources. Dynamic pages should be checked for changes on each page load.

Slovak’s picture

So, for static resources it is

        location ~* ^.+\.(jpg|jpeg|gif|png|ico)$ {
                access_log        off;
                expires 30d;
                break;
        }

Should css and js be included? Or if those are optimized by Drupal (included in one file) should they still be cached?

And for all other pages it is

location ~ .php$ {
...
}

Which covers all Drupal generated content.

brianmercer’s picture

That's good except you don't need the "break;" line.

Yes, css and js should have a similar expires time and can be included in that block. That's even if aggregated by Drupal. If you use Boost you need extra handling for css and js and if you use Imagecache you need extra handling for image files.

Omegacc's config on git has extra blocks for modules like Boost, Imagecache, Fckeditor and Backup_migrate and some extra security for things like not running .php from the files directory.

Slovak’s picture

Thanks for the response.

Omegacc's config on git

I am not familiar with that. Can you please provide a URL?

brianmercer’s picture

Slovak’s picture

Many thanks!

Slovak’s picture

I've also got gzip in nginx enabled and disabled in Drupal as not to duplicate the compression process.

MTecknology’s picture

I took the time to write up a best practices wiki page on the Nginx wiki for Drupal.

Your configurations may or may not be similar and may or may not add extras. The setup in the Nginx wiki will not cover things like using Boost or Varnish.

The configuration in the wiki will provide an excellent base for anything you want to do with Drupal (and Pressflow). It takes a lot of beginner mistakes and explains why they are the wrong way. There are a massive number of beginner mistakes that I've seen in this page and I hope that linking to this page will help.

Nginx Wiki - Drupal

Michael Lustfield
Ubuntu Member, Nginx Hacker

hedac’s picture

thanks
why this?

location ~ \..*/.*\.php$ {
                return 403;
        }

also
my rewrite is with last at the end
rewrite ^/(.*)$ /index.php?q=$1 last;
any reasons you don't use last?

another question.. why cgi.fix_pathinfo = 0 ?
It's working fine for me with default value which is 1 now

your location / didn't work for me.. giving me access denied... maybe I have old nginx.. but in 0.7.67 what works for me is without the uri/

location / {
try_files $uri @rewrite;
}
brianmercer’s picture

There are some long threads about "cgi.fix_pathinfo = 1" and why it is a security problem on the nginx boards, but I'll boil it down.

Drupal, Wordpress, etc. pass their parameters to scripts through queries, i.e. /index.php?q=user/login. It's hidden by clean urls, but that's how it does it.

Some software passes its info through uri path, i.e. /index.php/site/login. Chive is one. I think squirrelmail also. If you're using CGI to pass the request to a backend, you need to pass it in a parameter called PATH_INFO. The php devs have included a feature (unfortunately enabled by default) that does this.

This leads to a security problem with common nginx setups. Someone uploads a malicious php file to your Drupal server, for example, with a jpg extension. Then they go to http://mysite.com/sites/mysite.com/files/maliciousfile.jpg/nonsense.php.

Because it ends in .php common nginx configs just pass it to the php backend as "SCRIPTNAME nonsense.php". Due to "cgi.fix_pathinfo = 1", PHP then "fixes" the request by changing it to
"SCRIPTNAME malicious.jpg" and "PATH_INFO /nonsense.php" and executes malicious.jpg as a php script. Then nasty things happen.

Changing php.ini to "cgi.fix_pathinfo = 0" is the most rigorous fix. Igor added the fastcgi_split_path_info directive to deal with the needs of software that use PATH_INFO. My config for Chive is

location ~ ^(.+\.php)(.*)$ {
    include /etc/nginx/fastcgi_params;
    fastcgi_index index.php;
    fastcgi_split_path_info ^(.+\.php)(.*)$;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_pass php;
}

I am in favor of using exact names in my nginx configs where possible, especially for .php files. http://test.brianmercer.com/content/nginx-configuration-drupal. But when you post generic configs on the web you tend to make them loose so that special modules that have their own .php files will still work without changing the nginx config. Convenience or security, same as usual. (exact name matches are also faster than regexes, so if you're a performance nut, you'll use exact name matching as much as possible)

If you're just using Drupal, Wordpress, myphpadmin, etc. you can just "cgi.fix_pathinfo = 0" and be done with it. When you run into software that has the uri format "index.php/site/info" then you will need the extra parameters.

The long threads are http://forum.nginx.org/read.php?2,88845,88996 and http://forum.nginx.org/read.php?2,124297,124332

hedac’s picture

thank you very much for the explanation
so then I'll put "cgi.fix_pathinfo = 0"
but I have the configuration with exact names following your recommendation too. I had to add another php: serve.php for Advertisement module. But I prefer that way too. Faster and more secure. not generic but good for my needs.

I've just discovered the http://wiki.nginx.org/NginxHttpGzipStaticModule
do you have experience with this? I'm looking how to configure it. I'm using Boost drupal module now ok. and by default it says there is an option to make gzip files. It says not to activate it if you are using server gzip, but I think that by using nginx gzipstatic and boost.. it could save CPU by using gzip files directly and not compressing them on the fly.

brianmercer’s picture

Yes, gzip_static is definitely recommended with Boost. It works just fine. Anytime you request a file ending in .html it automatically checks if there's an identically named file ending in .html.gz in the same folder and serves it if it exists.

Yhager's config works fine for Boost and this is mine: http://test.brianmercer.com/content/nginx-configuration-drupal-boost

I have the "gzip_static on" directive in the nginx.conf. The version of nginx in recent Debian/Ubuntu repos have the gzip_static module compiled in.

hedac’s picture

thanks
I also recommend turning off this setting in boost "Aggressive Gzip: Deliver gzipped content independent of the request header."
because I was having errors in access log: GET /boost-gzip-cookie-test.html HTTP/1.1" 404

I'm very happy now with nginx and boost... :) I wonder how I lived without it !!!!!

in your nginx.conf you have a space in keepalive_timeout I guess you want to mean 55 not 5 5

in the backend.. I have no socket.. I just have
server 127.0.0.1:9000;

How can I create a socket? is it better to do with a socket?

brianmercer’s picture

Timeout: The two values are correct. See http://wiki.nginx.org/NginxHttpCoreModule#keepalive_timeout

Unix sockets vs TCP sockets: Unix sockets should be more efficient because there's no network overhead. They also can't be reached externally, so no need to firewall them. They use the file system so you can restrict them to the www-data user only and 600 permission them. They have to be on the same server, of course.

In practice a TCP socket is well able to handle huge amounts of traffic and some folks have experienced problems using Unix sockets with high traffic.

If you Google it, I'm sure you'll find lots of discussion with few definitive answers.

How you do it depends on how you're starting PHP. If you're using php-fpm it's a listen directive in the config file, and you have to set the user and permissions.

Yeah, I don't use the Aggressive Gzip. It's a neat idea, but I don't trust sending gzipped content to a client that doesn't say it accepts it.

Jonah Ellison’s picture

THANK YOU for posting this write up! I'm new to nginx and it provided a perfect explanation on how to avoid the common nginx pitfalls. This forum post is horribly outdated and unfortunately is the first result returned when searching for "drupal nginx"

Can someone please update this forum post to use the instructions from http://wiki.nginx.org/Drupal

perusio’s picture

that config is broken. Drupal doesn't use PATH_INFO. Go to the Nginx group to get a configuration that isn't insecure like the one on the Nginx wiki.

Marko B’s picture

perusio why do you say so? and then also point to a page that also has this wiki page set as example? i dont understand why people post so uninformative posts that just confuse people?!