Quick LNMP Server Setup Guide

Learn more about the inner workings of this website’s server and how to setup an LNMP stack.

Preamble

This is not a verbose instruction set for beginners. I also did not write portions of the guide and may refer you to external websites. All resources are accurate and tested on a fresh Ubuntu 8.10 (Intrepid) Slice as of the time of this writing. Following these steps will result in a skeleton server similar to what I use. In fact, this was part of my internal documentation which I decided to make public.

I gloss over some topics such as configuration files and tuning because it is not my intention to write a comprehensive guide. This is simply a quick check sheet that I use when rolling out a new server, but it might be useful to you.

General procedure

  • Initial prep and setup
    • Update using Aptitude
    • Create a non-privileged user
    • SSH public key authentication
  • Core software
    • MySQL
    • PHP with APC
    • Nginx
    • Email Services
  • Security
    • Disable root login
    • Firewall
    • Automatic security checks
    • Fine tooth comb run through with penetration testing
    • Monitoring
  • Extra software
    • logrotate
    • htop
    • traceroute

Initial Prep and Configuration

Update and install build tools.

sudo aptitude update
sudo aptitude safe-upgrade
sudo aptitude install build-essential

Create a non-privileged user

You should already know how to do this (hint: adduser jon).

SSH public key authentication

Let’s make logging into the server easier by setting up public key authentication. See How To: SSH Public Key Authentication.

Core Software

Install MySQL5 and PHP5 with APC.

sudo aptitude install mysql-server php5-cgi php5-cli php5-mysql php5-mysqli php5-curl php-apc

Setup PHP and fastcgi spawning

Create the following script in /etc/default/php-fastcgi

START=yes

# Which user runs PHP? (default: www-data)

EXEC_AS_USER=www-data

# Host and TCP port for FASTCGI-Listener (default: localhost:9000)

FCGI_HOST=localhost
FCGI_PORT=9000

# Environment variables, which are processed by PHP

PHP_FCGI_CHILDREN=4
PHP_FCGI_MAX_REQUESTS=500

Create the init script so that the PHP processes can be started and stopped (/etc/init.d/php-fastcgi) [source].

#! /bin/sh
## BEGIN INIT INFO
# Provides:          php-fastcgi
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start and stop php-cgi in external FASTCGI mode
# Description:       Start and stop php-cgi in external FASTCGI mode
## END INIT INFO

# Author: Kurt Zankl < [EMAIL PROTECTED]>

# Do NOT "set -e"

PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="php-cgi in external FASTCGI mode"
NAME=php-fastcgi
DAEMON=/usr/bin/php-cgi
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

# If the daemon is not enabled, give the user a warning and then exit,
# unless we are stopping the daemon
if [ "$START" != "yes" -a "$1" != "stop" ]; then
        log_warning_msg "To enable $NAME, edit /etc/default/$NAME and set 
START=yes"
        exit 0
fi

# Process configuration
export PHP_FCGI_CHILDREN PHP_FCGI_MAX_REQUESTS
DAEMON_ARGS="-q -b $FCGI_HOST:$FCGI_PORT"


do_start()
{
        # Return
        #   0 if daemon has been started
        #   1 if daemon was already running
        #   2 if daemon could not be started
        start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON 
--test > /dev/null \
                || return 1
        start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \
                --background --make-pidfile --chuid $EXEC_AS_USER --startas 
$DAEMON -- \
                $DAEMON_ARGS \
                || return 2
}

do_stop()
{
        # Return
        #   0 if daemon has been stopped
        #   1 if daemon was already stopped
        #   2 if daemon could not be stopped
        #   other if a failure occurred
        start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile 
$PIDFILE > /dev/null # --name $DAEMON
        RETVAL="$?"
        [ "$RETVAL" = 2 ] && return 2
        # Wait for children to finish too if this is a daemon that forks
        # and if the daemon is only ever run from this initscript.
        # If the above conditions are not satisfied then add some other code
        # that waits for the process to drop all resources that could be
        # needed by services started subsequently.  A last resort is to
        # sleep for some time.
        start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec 
$DAEMON
        [ "$?" = 2 ] && return 2
        # Many daemons don't delete their pidfiles when they exit.
        rm -f $PIDFILE
        return "$RETVAL"
}

case "$1" in
  start)
        [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
        do_start
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  stop)
        [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
        do_stop
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  restart|force-reload)
        log_daemon_msg "Restarting $DESC" "$NAME"
        do_stop
        case "$?" in
          0|1)
                do_start
                case "$?" in
                        0) log_end_msg 0 ;;
                        1) log_end_msg 1 ;; # Old process is still running
                        *) log_end_msg 1 ;; # Failed to start
                esac
                ;;
          *)
                # Failed to stop
                log_end_msg 1
                ;;
        esac
        ;;
  *)
        echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
        exit 3
        ;;
esac

Now finish up

sudo chmod +x /etc/init.d/php-fastcgi
sudo /usr/sbin/update-rc.d -f php-fastcgi defaults

Download, compile, and install Nginx.

sudo aptitude install libpcre3 libpcre3-dev libpcrecpp0 libssl-dev zlib1g-dev
mkdir ~/sources
cd ~/sources
# replace version numbers with most recent stable version
wget http://sysoev.ru/nginx/nginx-0.6.35.tar.gz

tar -zxvf nginx-0.6.35.tar.gz
cd nginx-0.6.35/

./configure --sbin-path=/usr/local/sbin --with-http_ssl_module --with-http_stub_status_module --pid-path=/var/run/nginx.pid --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log
make
# if upgrading, stop nginx first. Zero downtime upgrades are more complex. Reference the Nginx Wiki if it's that important.
make install

Create the Nginx init script (/etc/init.d/nginx)

#! /bin/sh

## BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the nginx web server
# Description:       starts nginx using start-stop-daemon
## END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/local/sbin/nginx
NAME=nginx
DESC=nginx

test -x $DAEMON || exit 0

# Include nginx defaults if available
if [ -f /etc/default/nginx ] ; then
. /etc/default/nginx
fi

set -e

. /lib/lsb/init-functions

case "$1" in
start)
echo -n "Starting $DESC: "
start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid \
--exec $DAEMON -- $DAEMON_OPTS || true
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \
--exec $DAEMON || true
echo "$NAME."
;;
restart|force-reload)
echo -n "Restarting $DESC: "
start-stop-daemon --stop --quiet --pidfile \
/var/run/$NAME.pid --exec $DAEMON || true
sleep 1
start-stop-daemon --start --quiet --pidfile \
/var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS || true
echo "$NAME."
;;
reload)
echo -n "Reloading $DESC configuration: "
start-stop-daemon --stop --signal HUP --quiet --pidfile /var/run/$NAME.pid \
--exec $DAEMON || true
echo "$NAME."
;;
status)
status_of_proc -p /var/run/$NAME.pid "$DAEMON" nginx && exit 0 || exit $?
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2
exit 1
;;
esac

exit 0

Finish preparing the init script.

sudo chmod +x /etc/init.d/nginx
sudo /usr/sbin/update-rc.d -f nginx defaults

Configure Nginx and your home directory structures

Refer to the following documents on creating your Nginx configuration and home directory structures:
Nginx Configuration Source Layout
Sample Nginx Configuration
Nginx Virtual Hosts – Part 1
Nginx Virtual Hosts – Part 2

In a nutshell: your config files should be in /usr/local/nginx/conf and your home directories could be laid out as follows.

/home//public_html//log
/home//public_html//backup
/home//public_html//public
/home//public_html//private

There is no right or wrong way (although it’s debatable), so use what works for you.

Setup Email Services

I was previously using a Postfix + Courier setup on Ubuntu Hardy/Intrepid, however I am now using Postfix + Dovecot for better performance.

Follow my new guide on Setting Up Email Services on Ubuntu Intrepid using Exim and Dovecot. Or, you can follow my old guide on Setting Up Email Services on Ubuntu Hardy using Postfix and Courier, which also works on Ubuntu Intrepid.

Security

Disable root login

Add your non-privileged user to sudoers using visudo and then disable root login in /etc/ssh/sshd_config. For added security, change your SSH port to something unusual. Finally, make sure that you are using very strong passwords across the board.

Firewall

Setup iptables to you hearts content. There is a simple tutorial under the corresponding section header on http://articles.slicehost.com/2008/11/28/ubuntu-intrepid-setup-page-1. Additionally, you should explore adaptive solutions such as fail2ban.

Automatic security checks

Scans with chkrootkit
Scans with rkhunter

Fine tooth comb through with penetration testing

No one knows your setup better than you, so think like a hacker to find the weak spots. For example, do you let people upload files? If so, can these uploaded files be executed? Make sure that your code is safe. There are enough security vulnerabilities introduced by other peoples code.

Monitoring

Put the final touches on by adding monitoring utilities. Personally, I’m using Pingdom with custom health monitoring scripts.

Other Programs

logrotate
You need to rotate the logs periodically so that they don’t become space hogs and bring everything to a crawl.

sudo aptitude install logrotate

Nginx was the only software that we installed outside of the package manager, so that’s the only one that we need to specifically setup with logrotate. All the other logs should theoretically be taken care of.

Create the logrotate rules for nginx in /etc/logrotate.d/nginx

/home/*/public_html/*/log/*.log /var/log/nginx/*.log {
daily
missingok
rotate 7
notifempty
create 640 root adm
sharedscripts
postrotate
[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
endscript
}

htop—htop is a very useful little program, but is not required.

sudo aptitude install htop

traceroute—Most useful networking tool in the world.

sudo aptitude install traceroute

unzip—Useful for those annoying files that are .zip and not .tar.gz

sudo aptitude install unzip

AWStats—Useful in processing Nginx logs.

sudo aptitude install awstats

Create the cache directory

sudo mkdir /var/cache/awstats

Now add your configurations in /etc/awstats. For example, /etc/awstats/awstats.jonsview.com.conf looks like

LogFile="/home/jon/public_html/jonsview.com/log/access.log"
LogFormat=1
DNSLookup=0
DirData="/var/cache/awstats/"
DirCgi="/cgi-bin"
DirIcons="/icon"
SiteDomain="jonsview.com"
AllowToUpdateStatsFromBrowser=1
AllowFullYearView=3

Because you’re using Nginx, the following Slicehost thread will be very beneficial on accessing your statistics: http://forum.slicehost.com/comments.php?DiscussionID=2561

You will probably want to setup automatic updating before the logs are rotated every night.

Change Log

3/27/2009 – Added PHP APC, unzip, AWStats, and fail2ban recommendation.

8 comments… add one
  • Sergey May 13, 2010 Link Reply

    root@ibm:~# /etc/init.d/php-fastcgi restart
    * Restarting php-cgi in external FASTCGI mode php-fastcgi [ OK ]

    Does that mean fastcgi spawning properly ?

    Though when I do start or stop it, I see following:

    root@ibm:~# /etc/init.d/php-fastcgi stop
    start-stop-daemon: warning: failed to kill 1101: No such process

    Any ideas why.

    Honestly I’ve never seen properly working php-cgi script which is so necessary for nginx. All I successfully used was either SPAWN-FASTSGI (from lighthttpd package) which I hate – not suitable for heavy load 2000 concurrent connection and it’s dead, or PHP-FPM which requires patching your PHP, means you loose the ability of smooth updating of your PHP.

    The only way out is to wait PHP 5.4 stable…. or perhaps somebody will write proper script to run php-cgi from PHP package.

  • Jon Stacey May 11, 2010 Link Reply

    There were some HTML entities messing things up. I think I got them all. Here’s the original source: http://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg352883.html.

  • Sergey May 11, 2010 Link Reply

    Unfortunately I’ve stucked with the following problem (Ubuntu 10.04)

    When I try to start your script:
    /etc/init.d/php-fastcgi start

    I get following:
    /etc/init.d/php-fastcgi: 28: Syntax error: “&” unexpected

    Any ideas why ?

  • the_guv May 11, 2010 Link Reply

    interestin comments Jon and Sergey .. pleased I subbed, didn’t know that about APC.

    thought you both may be interested in some fresh fastCGI benchmarking:-

    PHP BENCHMARKED: PHP-FPM (integrated) vs PHP-FPM (separate) vs Spawn-FCGI vs FastCGI

    .. of course, with PHP’s announcement to take FPM into core code come, er, I think 5.4, this issue will be redundant (as will my benchmarking!)

    • Asal Dec 27, 2012 Link Reply

      So, that’s it? Everything is OK now? WOW! How do I get in contact with you when I set up anethor one of these up? I am making sites like these all the time. If you can fix this maybe we can work out some kind of deal where I can pay you to help me set them up.Dude, what’s your paypal? I wanna send you some money. Is 20 bucks alright?

  • Sergey May 10, 2010 Link Reply

    Hi. I wonder why you decided to use original fast-cgi. Honestly I really like !!! Sure it must be stable due to it is original, gut not sure about speed.

    What do you think about PHP-FPM ? Personally I think it is the best spawning for fast-cgi, but not comfortable for administration (installation via patch, cannot patch already installed PHP).

    Same question about APC. Why not Xcache or Eaccelerator.

    Explanation would be nice 😉

    • Jon Stacey May 10, 2010 Link Reply

      Quite simply, economics and using the best tool for the job. That’s one of the reasons I went back to a LAMP stack for this site. The time saved using the Debian packaging system far outweighed the time wasted maintaining Nginx and fast-cgi. My time is considerably more valuable to me than the cost of a more powerful server able to accommodate the larger stack.

      The reason I chose APC is because it will be part of core PHP at some point in the future, so it’s more likely to get special attention than the other alternatives. In the end, the Debian php-apc package worked out of the box and got the job done sufficiently.

      I toyed with PHP-FPM but maintaining a patched branch that has to be compiled after every update is something I do not want to worry about on a production server.

  • the_guv Oct 15, 2009 Link Reply

    I like that, Jon .. I like that a lot. I’d like it if you searched for similar stuff on my blog, we could compare notes, why not ..

    You’ve helped me on those log files here, especially .. most appreciated, thank you.

Leave a Comment

Time limit is exhausted. Please reload CAPTCHA.