Setting up a VPS for static site hosting

By Danny van Kooten on on Permalink.

Remember me moving this site over to sourcehut pages last week? It didn't last long. It doesn't have much to do with the service though1.

It's just that I used the last few weeks to move some friends and family back on shared hosting, which turned out to be amazing value for money. You get Apache + MySQL LTS + PHP 8.2 + 64 MB of Varnish for less than $10 per month, including SSH access and hourly back-ups.

With me going back to full-time employment in a few weeks, I really liked the idea of fewer responsibilities. So I migrated all my remaining PHP applications to that same shared hosting. With this site on sourcehut pages, I could then power down all of the virtual servers I was renting.

All was great for a couple of days, except for one thing... I missed having an easy option to run code connected to the internet. Especially since I am playing with the idea of allowing comments on this blog by allowing people to email to a specific address.

I decided to spin-up a cheap VPS again and use it to host my various static sites. hut publish is great, but so is rsync -rav. I'll use this post as documentation for future me, but hopefully it's of use to others in a similar boat too.

Server details

For the server we don't need much; a single core vCPU with 1 GB of RAM, IPv4 and IPv6 networking enabled, a bit of storage and Debian2 installed is plenty.

If your cloud provider has an option to configure a firewall from their UI, configure it to only allow inbound traffic on port 22 (SSH), 80 (HTTP) and 443 (HTTPS).

Software

Once logged in, we'll update the base packages and add what we need.

apt update
apt upgrade
apt install vim nginx certbot python3-certbot-nginx

We could get somewhat newer versions by adding the nginx APT repository and using snap to install certbot, but I am going to be sticking to Debian packaged versions here.

Configuring nginx

We'll be storing our websites and configuration files in /var/www/.

Open up /etc/nginx/nginx.conf and add the following line inside the http { } block:

include /var/www/nginx/*

This instructs nginx to include all files in the /var/www/nginx directory, allowing us to leave the rest of this file alone.

Create said directory and in it, create a file called nginx.conf that will contain our global configuration (across all sites).

mkdir /var/www/nginx
touch /var/www/nginx/nginx.conf

The first thing we want to do is to disable the server_tokens directive, to stop including the nginx version in HTTP headers.

/var/www/nginx/nginx.conf
server_tokens off;

Next up is enabling gzip compression and configuring it properly.

/var/www/nginx/nginx.conf
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 32 4k;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;

This enables gzip compression for HTML, CSS, SVG and JS responses at a level that strikes a nice balance between compute cost and compression ratio.

Responses with a Content-Length header of less than 1024 bytes are not compressed, since they would barely benefit from it.

To determine a good setting for gzip_buffers, use getconf to get the size of a memory page on your system.

getconf PAGESIZE

If this returns a value other than 4096, modify the gzip_buffers setting accordingly.

Serving your site

Next up, create another file containing the server configuration for your static site.

/var/www/nginx/www.dannyvankooten.com
server {
    listen 80;
    listen [::]:80;
    index index.html;
    server_name www.dannyvankooten.com dannyvankooten.com;
    root /var/www/www.dannyvankooten.com;

    # Cache static assets for 1 year
    location ~* .(?:css|js|ico|txt|svg|jpg|jpeg|webp|png|csv)$ {
        expires 1y;
        add_header "Cache-Control" "public";
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

Test your configuration with nginx -t. If that succeeds, reload nginx with nginx -s reload.

Uploading your site

This site uses gozer to turn Markdown into HTML files and generate an RSS feed. Uploading the site to our server is a simple case of rsync:

rsync -rav build/. remote-user@remote-host:/var/www/www.dannyvankooten.com

The nice thing about this is that on subsequent calls, only modified files are transferred. We could use -z to enable compression, but the gains are fairly minimal because files are sent in isolation.

Update your DNS records

Our site is ready to go live. You can preview it by adding a temporary entry in your /etc/hosts file.

123.456.789.123 www.dannyvankooten.com dannyvankooten.com

If everything looks good, update the DNS records of your domain so it has an A and AAAA record pointing to your server.

You can verify the DNS change using dig:

dig dannyvankooten.com +noall +answer -t A
dig dannyvankooten.com +noall +answer -t AAAA

Enable HTTPS

With your domain pointing to your server, it is now time for the final step: enabling HTTPS on your site. We already have Certbot installed, so creating a new SSL certificate that automatically renews every 3 months is as easy as:

certbot --nginx -d dannyvankooten.com,www.dannyvankooten.com

That's it! You can repeat the steps above for multiple sites. Unless your blog is getting millions of pageviews per dag, you can easily host a dozen of static sites like this without your server really having to work.

Tweaks

What follows are some final tweaks, Not strictly necessary but nice nonetheless:

Creating a non-root user

It's always a good idea to create a non-privileged user account that requires sudo to perform actions that require elevated permissions.

adduser danny
adduser danny sudo
adduser danny www-data

Only allow SSH access using PubKeyAuthentication

First, make sure to add your public key to $HOME/<user>/.ssh/authorized_keys.

Then open up /etc/ssh/sshd_config and disable password authentication.

/etc/ssh/sshd_config
PasswordAuthentication no

Increasing soft limit for open file descriptors

nginx defaults to 768 worker_connections. We should increase our soft limit for open file descriptors to at least twice that value.

Open up /etc/security/limits.conf and add the following line just before the end of the file:

/etc/security/limits.conf
www-data soft nofile 1536

Disabling or buffering access logging

Logging every request consumes both CPU and I/O cycles. You can disable it entirely by including the following directive in your configuration file.

/var/www/nginx/nginx.conf
access_log off;

Another way to reduce the impact is to enable access log buffering.

/var/www/nginx/nginx.conf
access_log /var/log/nginx/access.log combined buffer=4096 flush=1m;

This will only write to the log once the 4 kB buffer is full or if a minute has passed since the last write.

Limit size of request body

Since we're hosting a static website that does not accept any request data, we can change the default value for client_max_body_size to something much closer to zero.

/var/www/nginx/nginx.conf
client_max_body_size 4k;

  1. If I had to nitpick two things it is that they do seem to be somewhat less reliable in terms of uptime since they suffered a huge DDOS a while ago. Also, I am unsure whether their (new) servers are powered by renewable energy↩︎

  2. You can of course pick other distributions. A lot of tutorials online are using Ubuntu, but I really enjoy Debian (which Ubuntu is based on) and its stability. ↩︎


Comments are welcome. You can email me at hi @ this domain.