Jon McPartland

Web developer

Building a web server from a fresh VPS

When I was building the VPS on DigitalOcean for this site to run on, I decided that I wanted to configure it all myself, rather than use the straightforward one-click install that they provide, because I always find that doing things myself is more rewarding. I decided to document the entire process, as it made for a good learning experience for me.

Basic Server Setup

I started with a bare Ubuntu 14.04 box, which takes but a minute to spin up on Digital Ocean.

  • First thing’s first, I set the server’s timezone, so that it would report the correct local time: dpkg-reconfigure tzdata -> Europe -> London
  • I then added NTP sync with apt-get install ntp
  • I wanted to add a swapfile, since this is a $5 box with only 512MB of RAM:
    • fallocate -l 4G /swapfile
    • chmod 600 /swapfile
    • mkswap /swapfile
    • swapon /swapfile
    • sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'

Users and SSH

I wanted to disable root access via SSH—as it's never a good idea to expose the superuser to the 'wild', so to speak—without disabling SSH all together. to do that, I'd need to create a new user.

  • adduser demo creates a new user. To give the new user sudo privileges, they need to be added to the sudoers file. To do this, visudo needs to be run (as root), and the line demo ALL=(ALL:ALL) ALL should be added below the root user specification. With the new user created, I'd have to enable access to their account via SSH by adding my SSH key into their profile. I first copied my client ssh key to my clipboard by running pbcopy < ~/.ssh/id_rsa.pub on the client. Then, on the server, I switched to the new account to perform the necessary steps with su - demo.
  • mkdir ~/.ssh && chmod 700 ~/.ssh
  • nano ~/.ssh/authorized_keys
  • [Save + Exit nano] chmod 600 ~/.ssh/authorized_keys
  • I disabled SSH access for the root user by editing /etc/ssh/sshd_config, and changing the line PermitRootLogin to no, and restarted the SSH daemon with service ssh restart
  • Before terminating the root user SSH session, I checked that I was able to SSH into the server with the new user ssh demo@XXX.XXX.XXX.XXX

Firewall

In addition to preventing SSH access to the root user, I wanted to mitigate against brute force attempts to login as my user. For this, I decided to use a package called fail2ban, which can automatically add rules to iptables when it encounters suspicious behaviour.

  • After installing fail2ban, and associated relations (sudo apt-get install fail2ban sendmail iptables-persistent), I created a local copy of its configuration file sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local.
  • I modified a few settings in this new file:
    • bantime = 1800
    • destemail = my@email.address
    • action = %(action_mwl)s
  • I also enabled the filters I wanted fail2ban to protect, by changing enabled = true for each service (SSH, Apache, etc), saved the file, and restarted the service sudo service fail2ban restart

I then set up a basic firewall via ufw (sudo apt-get install ufw), which allows you to add rules to iptables in a less verbose manner.

sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow out 80/tcp
sudo ufw allow out 53/tcp
sudo ufw allow out 53/udp
sudo ufw enable

This setup will drop any traffic that isn’t aimed at ports 22 or 80, and fail2ban will protect port 22 against brute force attack.

Git

I wanted to be able to automatically deploy any changes to my site without having to use a third-party service, by simply pushing my code to a git repository on the server. This turned out to be fairly straightforward.

  • Firstly, I needed git: sudo apt-get install git
  • I then created a directory in which I could store my repository: mkdir /var/repo
  • I then instantiated git within that directory: git init --bare mysite.git

ssh://demo@XXX.XXX.XXX.XXX:/var/repo/mysite.git would now function as a git remote for my project. However, I didn’t want my git directory to be my web server directory; I’d rather host it from /var/www/mysite. Thanks to git hooks, this is a doddle. I simply needed to set up a post-receive hook within my repository to point the working directory at the directory I wanted.

  • Editing the appropriate file: nano /var/repo/mysite.git/hooks/post-receive, I added the below:
#!/bin/sh
git --work-tree=/var/www/mysite --git-dir=/var/repo/mysite.git checkout -f

This separates out the working tree from the git directory. Marvellous.

Web Server

I was now ready to set up my web server stack. I wanted to use Apache with Google’s Pagespeed module, with Varnish caching rendered views, and Memcached caching queries and sessions.

  • I grabbed the necessary packages with sudo apt-get install apache2 libapache2-mod-auth-mysql php5-mysql mysql-server php5 libapache2-mod-php5 php5-mcrypt php5-cli php5-gd php5-memcached
  • I had to add Varnish to my list of sources
    • sudo apt-get install apt-transport-https
    • curl https://repo.varnish-cache.org/GPG-key.txt | sudo apt-key add -
    • sudo touch /etc/apt/sources.list.d/varnish-cache.list
    • sudo echo "deb https://repo.varnish-cache.org/ubuntu trusty varnish-4.1" >> /etc/apt/sources.list.d/varnish-cache.list
    • sudo apt-get update && sudo apt-get install varnish
  • I had to manually download the Pagespeed module
    • cd /tmp
    • wget https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-beta_current_amd64.deb
    • sudo dpkg -i mod-pagespeed-*.deb
    • sudo apt-get -f install

Apache Config

I started by disabling the default vhost, and adding my server configuration.

  • sudo a2dissite 000-default
  • sudo nano /etc/apache2/sites-available/mysite.conf
<VirtualHost *:9080>
    ServerName jon.mcpart.land
    DocumentRoot /var/www/mysite/public

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    <Directory /var/www/mysite/public>
        Options FollowSymLinks Multiviews
        AllowOverride All
        Require all granted
        Allow from all
    </Directory>
</VirtualHost>
  • sudo nano /etc/apache2/ports.conf
Listen 9080
  • sudo a2ensite mysite
  • sudo nano /etc/apache2/conf-available/pagespeed.conf
ModPagespeed on
ModPagespeedInheritVHostConfig on
ModPagespeedFileCachePath "/var/cache/mod_pagespeed/"
ModPagespeedPreserveUrlRelativity on
ModPagespeedRespectVary off
ModPagespeedDisableRewriteOnNoTransform off
ModPagespeedLowercaseHtmlNames on
ModPagespeedModifyCachingHeaders on
ModPagespeedListOutstandingUrlsOnError on

# force mod_pagespeed to send optimised pages to varnish
ModPagespeedBlockingRewriteKey "throwtovarnish"

AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html

ModPagespeedEnableFilters insert_dns_prefetch,remove_quotes
  • sudo a2enconf pagespeed
  • sudo a2enmod pagespeed
  • sudo service apache2 restart

MySQL Config

  • sudo mysql_install_db
  • sudo /usr/bin/mysql_secure_installation
  • mysql -uroot -p
    • create database mysite;
    • grant all on mysite.* to mysite@localhost identified by 'password';
    • flush privileges;

Varnish Config

  • /etc/varnish/default.vcl
vcl 4.0;

import std;
import directors;

backend default {
    .host = "127.0.0.1";
    .port = "9080";
    .connect_timeout = 60s;
    .first_byte_timeout = 60s;
    .between_bytes_timeout = 60s;
}

acl purge {
    "localhost";
    "127.0.0.1";
}

sub vcl_init {
    new cluster = directors.round_robin();
    cluster.add_backend(default);
}

sub vcl_recv {
    # Happens before we check if we have this in cache already.
    #
    # Typically you clean up the request here, removing cookies you don't need,
    # rewriting the request, etc.

    set req.backend_hint = cluster.backend();

    if (!std.healthy(req.backend_hint)) {
        unset req.http.Cookie;
    }

    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return (synth(405, "Not allowed."));
        }

        return (purge);
    }

    if (req.url ~ "^/admin/.*$") {
        return (pass);
    }

    # receive optimised pages from mod_pagespeed
    set req.http.X-PSA-Blocking-Rewrite = "throwtovarnish";
}

sub vcl_backend_response {
    # Happens after we have read the response headers from the backend.
    #
    # Here you clean the response headers, removing silly Set-Cookie headers
    # and other mistakes your backend does.

    set beresp.grace = 6h;
}

sub vcl_backend_error {
    set beresp.http.Content-Type = "text/html; charset=utf-8";

    synthetic("<div>server has fallen over.</div>");

    return (deliver);
}

sub vcl_deliver {
    # Happens when we have all the pieces we need, and are about to send the
    # response to the client.
    #
    # You can do accounting or modifying the final object here.
}
  • /etc/default/varnish
START=yes
NFILES=131072
MEMLOCK=82000

DAEMON_OPTS=“-a :80 \
             -T localhost:6082 \
             -f /etc/varnish/default.vcl \
             -S /etc/varnish/secret \
             -s malloc,256m”

Memcached Config

Memcached automatically installs a daemon for itself, and the framework I used to build my site works with it out of the box, so I didn't need to customise any configuration for it.

Job Done!

That’s it. It may seem a bit of a faff, but I'd rather spend time getting a server set up properly, than throw one together quickly that could be vulnerable to all kinds of potential issues. Having Varnish and Memcached reduces a lot of the PHP & MySQL overhead, too, so the site stays nice and snappy.

if you have any questions or feedback, please leave them in the comments below.