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.