Ubuntu 12.04 LTS LEMP Server Setup

This will cover getting an Ubuntu 12.04 LTS server up and ready for use. Where my previous article concentrated on building a LAMP stack, this article will use Nginx (the "e", as in "engine" in LEMP).

Once again, this is not meant to be complete in terms of security.

Why Nginx?

Many people use Nginx over Apache because it's a "lighter" web server - it has less "feature bloat".

However, it's architecture is a large reason why it can handle many more concurrent conections than Apache.

It's event-based (non-blocking), which lets it take advantage of similar memory-saving techniques that Nodejs enjoys. For instance, it doesn't need to spawn a new thread with each request.

Its this architecture and lower memory requirements that make it capable of serving many more concurrent requests than Apache.

Additionally, Nginx is sometimes put in "front" of other web servers such as Apache. This is because it can act as a reverse-proxy - it can act as a load-balancer, passing requests off to multiple servers.

In the following setup, Nginx will handle static-file requests itself while passing off PHP requests to PHP-FPM.

Setup

This will install some basic packages, including PHP-FPM.

$ sudo apt-get update
$ sudo apt-get install vim		# Everyone likes vim, right?
$ sudo apt-get install build-essential
$ sudo apt-get install python-software-properties

# Run these steps if you want php 5.4, rather than 5.3
$ sudo add-apt-repository ppa:ondrej/php5
$ sudo apt-get update

# Install PHP-FPM
$ sudo apt-get install php5-fpm

# We also need to install php5-cli to run php in CLI with the usual "php" command
$ sudo apt-get install php5-cli

# Other general PHP needs
$ sudo apt-get install php5-mysql
$ sudo apt-get install php5-curl
$ sudo apt-get install php5-gd
$ sudo apt-get install php5-mcrypt

# Let's install MySQL also
$ sudo apt-get install mysql-server

Git

You may need Git on your server depending on your deployment strategy, or to support package managers such as Composer.

$ sudo apt-get install git-core

Composer

If you use Composer, you should also have it on your production server to pull in dependencies. Note: For production use, you should lock in your dependency version numbers. That way you won't get any surprises when you update composer packagers on your live server.

This installer uses "php" and can't be piped "php-fpm", which is why we installed the php5-cli package.

# Install composer globally
$ curl -sS https://getcomposer.org/installer | php
$ sudo mv composer.phar /usr/local/bin/composer

Nginx

Now that we have our basics installed, let's get on with installing Nginx.

$ sudo apt-get install nginx
$ sudo service nginx start 	# Doesn't start itself upon install

Now, since the priority on Nginx often revolves around site speed, I'll go over some settings which will set up some best-practices for cacheing, gzip as well as re-writing to index.php and so on.

Much of this will pull from H5BP's repository on nginx setup.

First, let's grab a better list of MIME types for Nginx to use:

# I ran this as root You may need to curl <URL> | sudo tee /etc/nginx/mime.types
$ curl https://raw.github.com/h5bp/server-configs-nginx/master/mime.types > /etc/nginx/mime.types

Second, let's update Nginx's main config file. Edit /etc/nginx/nginx.conf.

$ vim /etc/nginx/nginx.conf

Ubuntu creates Nginx with to run as user "www-data". I also set it to run as group "www-data", similar to Apache.

user www-data www-data;   	# Add www-data group name

The following defaults to 4, but I've set worker_processes to 2 for my hosting. This usually is set to 2 times the number of cores. I'm cheap and bought a single-core, hence I use 2 worker processes.

worker_processes 2;

Coinciding with the number of worker processes, we can set the number of available connections. We like to have this high, because we all have super-popular blogs. We'll also set the number of file handlers to something larger than the number of worker connections.

events {
	worker_connections 8000; # Number of open network connections
}

# Number of file handles per worker
# Each TCP connection is a file handler
# so needs to be larger than # connections
worker_rlimit_nofile 10000;

In the same file, inside of the http block, we're gonna do a few things. These are commented on below, and most are taken from the h5bp git repository.

Note: Do not replace your http {} block completely with this one - instead change or add the below directives as needed. This is not a full nginx.conf file.

http {
	
	# Don't display nginx version in headers
	server_tokens off;		

	# Improve log formatting
	log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for"';
    
    # Decrease the amount of time a request can sit idle
    keepalive_timeout 20;
    
    # Speed up file transfers
    sendfile        on;
    
    # Don't send out partial friends in packets
    tcp_nopush      on;
    
    # Collate smaller packets into fewer larger ones
    tcp_nodelay     off;
    
    # Enable gzip compression
    gzip on;
    gzip_http_version  1.0;  	# Works for http 1.1 and 1.0
    gzip_comp_level 5;			# Increase level of compression
    gzip_min_length 256;		# Don't compress small files, compressed versions can end up larger
    gzip_proxied any;			# Compress files for proxies as well
    gzip_vary on;				# Proxies cache both reglar and gzipped versions of file
    gzip_types					# Gzip the following types of requests/files
		application/atom+xml
		application/javascript
		application/json
		application/rss+xml
		application/vnd.ms-fontobject
		application/x-font-ttf
		application/x-web-app-manifest+json
		application/xhtml+xml
		application/xml
		font/opentype
		image/svg+xml
		image/x-icon
		text/css
		text/plain
		text/x-component;
    
}

That's it for the nginx.conf. Next, lets setup a virtual host.

$ cp /etc/nginx/sites-available/default /etc/nginx/sites-available/example
$ vim /etc/nginx/sites-available/example

Let's edit the new "example" virtual host.

# Redirect to non-www
server {
	server_name *.example.com;
    return 301 $scheme://example.com$request_uri;
}

server {
	
	# Document root
    root /var/www/example.com;
    
    # Try static files first, then php
    index index.html index.htm index.php;
	
	# Specific logs for this vhost
	access_log /var/log/nginx/example.com-access.log;
	error_log  /var/log/nginx/example.com-error.log error;
	
    # Make site accessible from http://localhost/
    server_name example.com;
	
	# Specify a character set
    charset utf-8;
	
	# h5bp nginx configs
    include conf/h5bp.conf;
	
	# Redirect needed to "hide" index.php
	location / {
            try_files $uri $uri/ /index.php?q=$uri&$args;
    }
	
	# Don't log robots.txt or favicon.ico files
	location = /favicon.ico { log_not_found off; access_log off; }
	location = /robots.txt  { access_log off; log_not_found off; }
	
	# 404 errors handled by our application, for instance Laravel or CodeIgniter
	error_page 404 /index.php;
	
	# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini

            # With php5-cgi alone:
            # fastcgi_pass 127.0.0.1:9000;
            # With php5-fpm:
            fastcgi_pass unix:/var/run/php5-fpm.sock;
            fastcgi_index index.php;
            include fastcgi_params;
    }
    
    # Deny access to .htaccess
    location ~ /\.ht {
            deny all;
    }        
    
}

Notice that we included a conf/h5bp.conf file in there. This adds some sensible defaults as per here, including cache expration, protecting system files, allowing cross-domain access for web fonts and some IE help. These lives in /etc/nginx/conf.

$ mkdir /etc/nginx/conf
$ curl https://raw.github.com/h5bp/server-configs-nginx/master/conf/h5bp.conf > /etc/nginx/conf/h5bp.conf
$ curl https://raw.github.com/h5bp/server-configs-nginx/master/conf/expires.conf > /etc/nginx/conf/expires.conf
$ curl https://raw.github.com/h5bp/server-configs-nginx/master/conf/x-ua-compatible.conf > /etc/nginx/conf/x-ua-compatible.conf
$ curl https://raw.github.com/h5bp/server-configs-nginx/master/conf/protect-system-files.conf > /etc/nginx/conf/protect-system-files.conf
$ curl https://raw.github.com/h5bp/server-configs-nginx/master/conf/cross-domain-fonts.conf > cross-domain-fonts.conf

That's it for Nginx config. Once your vhost is setup, you need to add it to the /etc/nginx/sites-enabled directory. This is done with a symlink:

$ sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/example

Don't forget to reload Nginx once your done with the configuration:

$ sudo service nginx reload

PHP Cleanup

There's some remaining PHP cleanup to do, similar to Apache setup.

$ sudo vim /etc/php5/fpm/php.ini
> cgi.fix_pathinfo=0  					# Change from 1 (exact file paths required)
> post_max_size = 8M					# Change to 8M
> upload_max_filesize = 8M				# Change from 2M
> max_file_uploads = 5					# Change from 20
> expose_php = off						# Don't display php version

# Restarting nginx doesn't affect this, need to reload php5-fpm
$ service php5-fpm restart

Firewall

This is exactly as per here. It will allow port 22 (or current ssh port), 80 and 443 (ssh, web traffic, ssl web traffic respectively). It also gives loopback access, important if your server is virtualized (chances are, it is).

# Run as root or use sudo
$ sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$ sudo iptables -A INPUT -p tcp --dport ssh -j ACCEPT
$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
$ sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
$ sudo iptables -A INPUT -j DROP
$ sudo iptables -I INPUT 1 -i lo -j ACCEPT

# Install and use so firewalls are saved through restarts
$ sudo apt-get install -y iptables-persistent
$ sudo service iptables-persistent start

Add MySQL user

This will create a MySQL user for your application to use. Note this grants all permissions. You should be as restrictive as possible, giving only what's necessary. In many instances, MySQL users will will be OK with simply having SELECT, UPDATE, DELETE, and INSERT privileges, but there are many more privileges to choose from. Any user who will be used to mysqldump will need the LOCK TABLES privilege.

Also note that if you have a MySQL database on a separate server, you'll need to change localhost to the IP or host name of the server connecting to the database. There is also some more work to allow remote connections on MySQL (Editing my.cnf bind-address and using firewalls to only allow MySQL connections on the same local network is one strategy).

$ mysql -u root -p
> CREATE USER 'user'@'localhost' IDENTIFIED BY 'password';
> GRANT ALL PRIVILEGES ON database.* TO 'user'@'localhost';

Some More Security

Here we'll make some security tweaks.

First, some providers allow root login via SSH. We want to turn that off. I suggest opening a new SSH connection immediately after creating a sudo user (in a separate Terminal session/window) before doing this, just in case you lock yourself out by accident.

If your provider gives you a login other than "root", then you likely have a sudo user already and can skip this. However, ensure you cannot log in as root via SSH.

# Don't let root ssh in
$ adduser mysudouser			# Create user
$ usermod -G sudo mysudouser	# Make user a sudo user (sudoer)

# (Log in and make sure this sudo user does indeed have the sudo permissiosn)

$ sudo vim /etc/ssh/sshd_config	
> PermitRootLogin no 			# Change from yes
$ sudo reload ssh

I typically also create a user for deployment. This user will share the same primary group as Nginx (www-data), and so will be able to read/write the web-server files. This is not a sudo user.

# Deploy user
$ adduser mydeployuser
$ usermod -g www-data mydeployuser

Web Root

The directory /var/www/example.com/public is the main web-root. In our configuration, Nginx will use this one (It technically has a default /usr/share/nginx/www as defined in /etc/nginx/sites-available/default). Instead of making this directory editable by the web root, we'll make its contents owned by www-data.

This way the Nginx and the 'deploy' user are the only ones who can read/write web files (without sudo privileges). Note: This makes use of group permissions. The following is saying "Users and Groups can read and write these files, but other users can only read them".

# Assuming /var/www/example.com/public is the web root
$ sudo chown -R www-data:www-data /var/www/example.com # make sure same owner:group
# Remove all group/other permissions
$ sudo chmod -R go-rwx /var/www/example.com
# Add group read/write
$ sudo chmod -R g+rw /var/www/example.com
# Allow other to read only
$ sudo chomd -R o+r /var/www/example.com

That's it for now - you should be able to be going with a LEMP stack now. I haven't installed an SSL certificate on an Nginx server yet - that'll likely come in the future.