HOWTO: Linux + Nginx + PHP + MySQL complete installation

Preface

There are many articles on web state that the scalability of Nginx is much better then Apache. We want to test building a hosting environment that use Nginx instead of Apache. And we can test the stability, scalability and etc on it. To maximize our control on the entire hosting environment, we'll compile everything from scratch. We'd also want to make binary backup-and-restore easier by putting everything inside "/usr/local" instead of default locations.

This is the summary on what we'll do:

  1. MySQL installation
    1. download the latest mysql community server (at the time, version 5.0.67) from web
    2. compile and install mysql to /usr/local/mysql-5.0.67
    3. put the data directory in /data/mysql-5.0 (for management practice)
    4. place the init.d file inplace
    5. start the mysql server
  2. PHP Fastcgi (with spawn-fcgi) installation
    1. download the latest php (at the time, version 5.3.0)
    2. compile and install php to /usr/local/php-5.3.0-fastcgi
    3. download the latest lighttpd (at the time, version 1.4.19)
    4. compile lighttpd
    5. copy "spawn-fcgi" from the compiled source of lighttpd to /usr/local/php-5.3.0-fastcgi/bin
    6. place the php fastcgi start script in place
    7. start the php fastcgi
  3. EngineX (Nginx) installation
    1. download the latest EngineX aka nginx (at the time, version 0.6.32)
    2. compile and install nginx to /usr/local/nginx-0.6.32
    3. configure nginx to work with php fastcgi
    4. start the nginx
  4. Testing and Have Fun :)

 

MySQL installation

  1. Download MySQL server
    wget http://dev.mysql.com/get/Downloads/MySQL-5.0/mysql-5.0.67.tar.gz/from/ftp://ftp.iij.ad.jp/pub/db/mysql/
    tar -xzf mysql-5.0.67.tar.gz
    cd mysql-5.0.67

     
  2. Configuring MySQL Compilation
    vi CONFIG.sh
    Code
    #!/bin/bash

    # correct stupid permission
    # in original package
    FNM_OWN=$(readlink -f $0)
    PWD_OWN=$(pwd)
    cd ..
    chown -R root.root $PWD_OWN
    cd $PWD_OWN
    chown -R root.root *
    find -type f | xargs chmod 644
    find -type d | xargs chmod 755
    find -type f -name '*.sh' | xargs chmod 700
    chmod 700 configure
    chmod 700 install-sh
    chmod 700 $FNM_OWN

    # configure
    ./configure \
     --prefix="/usr/local/mysql-5.0.67" \
     --enable-thread-safe-client \
     --with-extra-charsets=all
     

     
    chmod 700 CONFIG.sh
     

  3. Compile and Install MySQL
    ./CONFIG.sh && make && make install
     
  4. Create the init script (in Red Hat style)
    vi /etc/init.d/mysqld.custom
    Code
    #!/bin/bash
    #
    # mysqld        This shell script takes care of starting and stopping
    #               the MySQL subsystem (mysqld).
    #
    # chkconfig: - 64 36
    # description:  MySQL database server.
    # processname: mysqld
    # config: $prefix/etc/my.cnf
    # pidfile: /var/run/mysqld/mysqld.pid

    # Source function library.
    . /etc/rc.d/init.d/functions

    # Source networking configuration.
    . /etc/sysconfig/network

    prog="MySQL (Custom Installed)"
    prefix="/usr/local/mysql-5.0.67"
    my_print_defaults="$prefix/bin/my_print_defaults"

    # extract value of a MySQL option from config files
    # Usage: get_mysql_option SECTION VARNAME DEFAULT
    # result is returned in $result
    # We use my_print_defaults which prints all options from multiple files,
    # with the more specific ones later; hence take the last match.
    get_mysql_option(){
            result=`$my_print_defaults "$1" | sed -n "s/^--$2=//p" | tail -n 1`
            if [ -z "$result" ]; then
                # not found, use default
                result="$3"
            fi
    }

    get_mysql_option mysqld datadir "/var/lib/mysql"
    datadir="$result"
    get_mysql_option mysqld socket "$datadir/mysql.sock"
    socketfile="$result"
    get_mysql_option mysqld_safe log-error "/var/log/mysqld.log"
    errlogfile="$result"
    get_mysql_option mysqld_safe pid-file "/var/run/mysqld/mysqld.pid"
    mypidfile="$result"

    start(){
            touch "$errlogfile"
            chown mysql:mysql "$errlogfile"
            chmod 0640 "$errlogfile"
            [ -x /sbin/restorecon ] && /sbin/restorecon "$errlogfile"
            if [ ! -d "$datadir/mysql" ] ; then
                action $"Initializing MySQL database: " $prefix/bin/mysql_install_db
                ret=$?
                chown -R mysql:mysql "$datadir"
                if [ $ret -ne 0 ] ; then
                    return $ret
                fi
            fi
            chown mysql:mysql "$datadir"
            chmod 0755 "$datadir"
            # Pass all the options determined above, to ensure consistent behavior.
            # In many cases mysqld_safe would arrive at the same conclusions anyway
            # but we need to be sure.
            $prefix/bin/mysqld_safe   --datadir="$datadir" --socket="$socketfile" \
                    --log-error="$errlogfile" --pid-file="$mypidfile" \
                    >/dev/null 2>&1 &
            ret=$?
            # Spin for a maximum of N seconds waiting for the server to come up.
            # Rather than assuming we know a valid username, accept an "access
            # denied" response as meaning the server is functioning.
            if [ $ret -eq 0 ]; then
                STARTTIMEOUT=30
                while [ $STARTTIMEOUT -gt 0 ]; do
                    RESPONSE=`$prefix/bin/mysqladmin --socket="$socketfile" -uUNKNOWN_MYSQL_USER ping 2>&1` && break
                    echo "$RESPONSE" | grep -q "Access denied for user" && break
                    sleep 1
                    let STARTTIMEOUT=${STARTTIMEOUT}-1
                done
                if [ $STARTTIMEOUT -eq 0 ]; then
                        echo "Timeout error occurred trying to start MySQL Daemon."
                        action $"Starting $prog: " /bin/false
                        ret=1
                else
                        action $"Starting $prog: " /bin/true
                fi
            else
                action $"Starting $prog: " /bin/false
            fi
            [ $ret -eq 0 ] && touch /var/lock/subsys/mysqld
            return $ret
    }

    stop(){
            MYSQLPID=`cat "$mypidfile"  2>/dev/null `
            if [ -n "$MYSQLPID" ]; then
                /bin/kill "$MYSQLPID" >/dev/null 2>&1
                ret=$?
                if [ $ret -eq 0 ]; then
                    STOPTIMEOUT=60
                    while [ $STOPTIMEOUT -gt 0 ]; do
                        /bin/kill -0 "$MYSQLPID" >/dev/null 2>&1 || break
                        sleep 1
                        let STOPTIMEOUT=${STOPTIMEOUT}-1
                    done
                    if [ $STOPTIMEOUT -eq 0 ]; then
                        echo "Timeout error occurred trying to stop MySQL Daemon."
                        ret=1
                        action $"Stopping $prog: " /bin/false
                    else
                        rm -f /var/lock/subsys/mysqld
                        rm -f "$socketfile"
                        action $"Stopping $prog: " /bin/true
                    fi
                else
                    action $"Stopping $prog: " /bin/false
                fi
            else
                ret=1
                action $"Stopping $prog: " /bin/false
            fi
            return $ret
    }

    restart(){
        stop
        start
    }

    condrestart(){
        [ -e /var/lock/subsys/mysqld ] && restart || :
    }

    # See how we were called.
    case "$1" in
      start)
        start
        ;;
      stop)
        stop
        ;;
      status)
        status mysqld
        ;;
      restart)
        restart
        ;;
      condrestart)
        condrestart
        ;;
      *)
        echo $"Usage: $0 {start|stop|status|condrestart|restart}"
        exit 1
    esac

    exit $?
     

    chmod 755 mysqld.custom
    groupadd -g 27 mysql
    useradd -u 27 -g mysql -G mysql,daemon -d /var/lib/mysql -s /sbin/nologin -M mysql
    mkdir /var/run/mysqld
    chown mysql.mysql /var/run/mysqld
    chmod 770 /var/run/mysqld

     

  5. Config MySQL (my.cnf)
    mkdir /usr/local/mysql-5.0.67/etc
    vi /usr/local/mysql-5.0.67/etc/my.cnf

    Code
    [mysqld]
    datadir=/data/mysql-5.0
    socket=/tmp/mysql.sock
    user=mysql

    [mysqld_safe]
    log-error=/var/log/mysqld.log
    pid-file=/var/run/mysqld/mysqld.pid
     

     

  6. Start MySQL
    /etc/init.d/mysqld.custom start

 

PHP Fastcgi (with spawn-fcgi) Installation

After 2009/06, spawn-fcgi has become its own project, seperated from Lighttpd.
It should be available through here: http://redmine.lighttpd.net/projects/spawn-fcgi

  1. Download PHP
    wget http://hk2.php.net/get/php-5.3.0.tar.bz2/from/hk.php.net/mirror
    tar -xjf php-5.3.0.tar.bz2
    cd php-5.3.0

     
  2. Configuring PHP Compilation
    vi CONFIG.sh
    Code
    #!/bin/bash

    ./configure \
     --prefix="/usr/local/php-5.3.0-fastcgi" \
     --with-mysql="/usr/local/mysql-5.0.67" \
     --with-gd \
     --with-ttf \
     --with-openssl \
     --enable-mbstring \
     --enable-fastcgi
     

     

  3. Compile and Install PHP
    chmod 700 CONFIG.sh
    ./CONFIG.sh && make && make install

     
  4. Download Lighttpd
    wget http://www.lighttpd.net/download/spawn-fcgi-1.6.2.tar.bz2
    tar -xjf
    spawn-fcgi-1.6.2.tar.bz2
    cd spawn-fcgi-1.6.2
     
  5. Configure Lighttpd Compilation
    vi CONFIG.sh
    Code
    #!/bin/bash

    ./configure \
     --prefix="/usr/local/php-5.3.0-fastcgi"
     

     

  6. Compile and install
    chmod 700 CONFIG.sh
    ./CONFIG.sh
    && make && make install
     
  7. Create the init script
    vi /etc/init.d/php-fastcgi
    Code
    #!/bin/sh
    #
    # Spawn a fcgi processes
    # written by LAKostis <lakostis at altlinux.ru>
    #
    # chkconfig: 345 48 52
    # description: Spawn a fcgi processes
    # processname: /usr/bin/spawn-fcgi
    # config: /etc/sysconfig/spawn-fcgi
    # pidfile: /var/run/spawn-fcgi.pid

    WITHOUT_RC_COMPAT=1

    # Source function library.
    . /etc/init.d/functions

    PROG="PHP FastCGI (with spawn-fcgi)"
    NAME=php-fastcgi
    PREFIX=/usr/local/php-5.3.0-fastcgi
    PIDFILE=/var/run/php-fastcgi.pid
    LOCKFILE=/var/lock/subsys/php-fastcgi.lock

    ## ABSOLUTE path to the spawn-fcgi binary
    SPAWNFCGI="$PREFIX/bin/spawn-fcgi"

    ## ABSOLUTE path to the PHP binary
    FCGIPROGRAM="$PREFIX/bin/php-cgi"

    ## bind to tcp-port on localhost
    FCGISOCKET="/var/fastcgi/php-fastcgi.sock"

    ## number of PHP childs to spawn in addition to the default. Minimum of 2.
    ## Actual childs = PHP_FCGI_CHILDREN + 1
    PHP_FCGI_CHILDREN=20

    ## number of request server by a single php-process until is will be restarted
    PHP_FCGI_MAX_REQUESTS=1000

    ## IP adresses where PHP should access server connections from
    FCGI_WEB_SERVER_ADDRS="127.0.0.1"

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

    ## if this script is run as root switch to the following user
    USERID=daemon
    GROUPID=daemon

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

    RETVAL=0

    start() {
            echo -n $"Starting $prog: "

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

            export PHP_FCGI_MAX_REQUESTS
            export FCGI_WEB_SERVER_ADDRS

            ALLOWED_ENV="$ALLOWED_ENV PHP_FCGI_MAX_REQUESTS FCGI_WEB_SERVER_ADDRS"
           
            if [ !-d "$FCGISOCKETDIR" ]; then
                    mkdir $FCGISOCKETDIR
                    chown $USERID.$GROUPID $FCGISOCKETDIR
                    chmod 700 $FCGISOCKETDIR
            fi

            if test x$UID = x0; then
                    EX="$SPAWNFCGI -s $FCGISOCKET -f $FCGIPROGRAM -P $PIDFILE -u $USERID -g $GROUPID -C $PHP_FCGI_CHILDREN"
            else
                    EX="$SPAWNFCGI -s $FCGISOCKET -f $FCGIPROGRAM -P $PIDFILE -C $PHP_FCGI_CHILDREN"
            fi

            # 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
            env - $E $EX
            chmod 770 $FCGISOCKET

            RETVAL=$?
            echo
            [ $RETVAL = 0 ] && touch ${LOCKFILE}
            return $RETVAL
    }

    stop() {
            echo -n $"Stopping $PROG: "
            killproc $NAME php-fastcgi
            RETVAL=$?
            echo
            [ $RETVAL = 0 ] && rm -f ${LOCKFILE} ${PIDFILE}
    }
    reload() {
            echo -n $"Reloading $PROG: "
            killproc $NAME -HUP
            RETVAL=$?
            echo
    }

    case "$1" in
                    start)
                            start
                            ;;
                    stop)
                            stop
                            ;;
                    restart)
                            stop
                            start
                            ;;
                    status)
                            status --pidfile "$PIDFILE" --expect-user $USERID -- $PROG
                            RETVAL=$?
                            ;;
                    *)
                    msg_usage "${0##*/} {start|stop|restart|status}"
                    RETVAL=1
    esac

    exit $RETVAL
     

     

  8. Start the Fastcgi server
    /etc/init.d/php-fastcgi start

 
 

EngineX (Nginx) Installation

  1. Download Nginx
    wget http://sysoev.ru/nginx/nginx-0.6.32.tar.gz
    tar -xzf nginx-0.6.32.tar.gz

    cd nginx-0.6.32
     
  2. Configure Nginx Compilation
    vi CONFIG.sh
    Code
    #!/bin/bash

    ./configure \
     --prefix="/usr/local/nginx-0.6.32" \
     --with-openssl="/usr" \
     --with-md5="/usr" \
     --with-sha1="/usr"
     

     

  3. Compile and Install Nginx
    ./CONFIG.sh && make && make install
     
  4. Create a simple restart script for Nginx
    vi /usr/local/nginx-0.6.32/restart
    Code
    #!/bin/bash

    NGINX_DIR="/usr/local/nginx-0.6.32"
    NGINX_SBIN="$NGINX_DIR/sbin"

    stop() {
      if [ "$(pidof nginx)" != "" ]; then
        killall nginx
      fi
    }

    start() {
      $NGINX_SBIN/nginx
    }

    stop
    start
     

     

  5. Start Nginx
    /usr/local/nginx-0.6.32/restart.sh
     
  6. To further integrate it with RedHat base linux, you might add n init script like this
    vi /etc/init.d/nginx
    Code
    #!/bin/sh
    #
    # nginx - this script starts and stops the nginx daemon
    #
    # chkconfig:   - 85 15
    # description:  Nginx is an HTTP(S) server, HTTP(S) reverse \
    #               proxy and IMAP/POP3 proxy server
    # processname: nginx
    # config:      /etc/nginx/nginx.conf
    # config:      /etc/sysconfig/nginx
    # pidfile:     /var/run/nginx.pid

    # Source function library.
    . /etc/rc.d/init.d/functions

    # Source networking configuration.
    . /etc/sysconfig/network

    # Check that networking is up.
    [ "$NETWORKING" = "no" ] && exit 0

    prefix="/usr/local/nginx-0.6.38"
    nginx="$prefix/sbin/nginx"
    prog=$(basename $nginx)

    NGINX_CONF_FILE="$prefix/conf/nginx.conf"

    #[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx

    lockfile=/var/lock/subsys/nginx

    start() {
        [ -x $nginx ] || exit 5
        [ -f $NGINX_CONF_FILE ] || exit 6
        echo -n $"Starting $prog: "
        daemon $nginx -c $NGINX_CONF_FILE
        retval=$?
        echo
        [ $retval -eq 0 ] && touch $lockfile
        return $retval
    }

    stop() {
        echo -n $"Stopping $prog: "
        killproc $prog -QUIT
        retval=$?
        echo
        [ $retval -eq 0 ] && rm -f $lockfile
        return $retval
    }

    restart() {
        configtest || return $?
        stop
        sleep 1
        start
    }

    reload() {
        configtest || return $?
        echo -n $"Reloading $prog: "
        killproc $nginx -HUP
        RETVAL=$?
        echo
    }

    force_reload() {
        restart
    }

    configtest() {
      $nginx -t -c $NGINX_CONF_FILE
    }

    rh_status() {
        status $prog
    }

    rh_status_q() {
        rh_status >/dev/null 2>&1
    }

    case "$1" in
        start)
            rh_status_q && exit 0
            $1
            ;;
        stop)
            rh_status_q || exit 0
            $1
            ;;
        restart|configtest)
            $1
            ;;
        reload)
            rh_status_q || exit 7
            $1
            ;;
        force-reload)
            force_reload
            ;;
        status)
            rh_status
            ;;
        condrestart|try-restart)
            rh_status_q || exit 0
                ;;
        *)
            echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}"
            exit 2
    esac

    exit $RETVAL
     


    chmod 755 /etc/init.d/nginx
    chkconfig --add nginx
    chkconfig nginx on

     

  7. Open a browser and test!!!

 

Testing LEMP with Drupal

To further test this hosting platform, I've tested Drupal installation with it.

Apache support .htaccess file that let user to define folder specific config directive. Nginx does not support this. So instead of just putting an Apache hosting site on Nginx virtual host, we need to config Nginx site-by-site. This is quite bothering, but you can't have everything at the same time. So we started the testing.

  1. Configuring Nginx for virtual hosting
    The Default nginx.conf looks like this:
    Code
    #user  nobody;
    worker_processes  1;

    #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  0;
        keepalive_timeout  65;

        #gzip  on;

        server {
            listen       80;
            server_name  localhost;

            #charset koi8-r;

            #access_log  logs/host.access.log  main;

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

            #error_page  404              /404.html;

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

            # proxy the PHP scripts to Apache listening on 127.0.0.1:80
            #
            #location ~ \.php$ {
            #    proxy_pass   <a href="http://127.0.0.1;" title="http://127.0.0.1;">http://127.0.0.1;</a>
            #}

            # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
            #
            #location ~ \.php$ {
            #    root           html;
            #    fastcgi_pass   127.0.0.1:9000;
            #    fastcgi_index  index.php;
            #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
            #    include        fastcgi_params;
            #}

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

        # another virtual host using mix of IP-, name-, and port-based configuration
        #
        #server {
        #    listen       8000;
        #    listen       somename:8080;
        #    server_name  somename  alias  another.alias;

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

        # HTTPS server
        #
        #server {
        #    listen       443;
        #    server_name  localhost;

        #    ssl                  on;
        #    ssl_certificate      cert.pem;
        #    ssl_certificate_key  cert.key;

        #    ssl_session_timeout  5m;

        #    ssl_protocols  SSLv2 SSLv3 TLSv1;
        #    ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
        #    ssl_prefer_server_ciphers   on;

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

    }
     

    This is really easy to read. To add a virtual host, you may simply add this into http directive

    Code
        server {
            listen       10.0.0.50:80;    # change to fit your server
            server_name  example.com *.example.com;     # change to the hostname you want
            root   /var/www/somewhere;  # change to fit your hosting
            error_page   500 502 503 504  /50x.html;

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

            location = /50x.html {
                root   html; # you'll have default error pages here
            }
        }
     

     

  2. Configuring Nginx for PHP virtual hosting
  3. Configuring Nginx for Drupal (PHP virtual hosting with Rewrite)

 (still writing...)