Requirements

So, what do I need?

  • A laptop or desktop computer (running Linux, Windows, or macOS)
  • About $1 a month for your domain name
  • Either $5 a month for a VPS or one purchase of $60-$100 for a Raspberry Pi

Once you have this article pulled up on your computer, you’re ready to move on.

Getting started

Choosing a domain name

The first thing you will need is a domain name. This is the address that others can use to access your website. While most traffic will come from search engines, you still want a domain name which is short, has a clear connection to the content of the website, and is not too similar to that of an existing website, especially a popular one. mrlc.dev is the domain name of this site. mrlc is called the second-level domain, and I chose it to personally identify my website (it is my initials) and .dev is the top-level domain (TLD), which loosely corresponds to a general category of websites. In my case, .dev websites are intended for developers.

Most people from the United States will want a .com domain (especially for business purposes) or maybe a .org domain. These TLDs are among the most common, and with most domain providers cost $8-$12 per year to hold. Those in other countries who would like a country-specific domain should look up the TLD for their country.

Buying the domain name

Now that you have decided which domain to use, it’s time to buy it. My first recommendation for a domain provider (also known as registrar) is Njalla because they claim to be privacy conscious, although it comes at a premium (~$20/year vs. ~$12/year). If you are fine with sacrificing some privacy for a cheaper domain, then other reputable providers include Epik, namecheap, and Google Domains, of which namecheap is usually the cheapest. Search for your desired domain name with your chosen provider to make sure that it’s not taken. If not, you’re in luck!

As you are checking out, pay attention to the renewal and domain privacy settings, which both vary by provider. If you know that you will only use your website for less than a year, make sure to turn off auto-renewal. More importantly, make sure that domain privacy is enabled; some providers have you pay extra for this. Domain privacy prevents your personal information (name, address, phone number, etc.) from being directly associated with the website by substituting yours for the contact information of the registrar. Once you have checked out, we can proceed to hosting the website.

Setting up the hosting platform

Virtual private server vs. self-hosting

If you would like to save the $5 a month normally spent on a virtual private server (VPS), you can opt to host the website yourself. However, there are a few downsides:

  • You need a computer to act as your server. This can be your desktop or even laptop (not recommended) although you should recognize that your website will not be accessible so long as your computer is powered off. A better choice which uses less power than a traditional computer but is still good for hosting a website is a mini-computer called the Raspberry Pi. You can buy kits from the Raspberry Pi Foundation which include the Pi itself as well as necessary accessories for between $60 and $100 here. Any kit which is branded as a “desktop kit” or “personal computer kit” should be enough to host a website.
  • Excessive traffic to your website may cause your home network to slow down. In addition, self-hosting method can identity your home IPV4 address with your website.
  • You need access to your router’s administrative settings, which excludes work or school networks.

If you decide to use a VPS rather than self-host, continue to the next section, on choosing a VPS provider. If you choose to self-host, stay tuned for a future article.

Choosing a VPS provider

While people will access your website through its domain name, your website will actually run on a virtual private server (VPS) with a publicly-accessible IPV4 address (to be explained in greater detail later). There are many different VPS providers, but I would again recommend Njalla for those who cannot sacrifice privacy. Other reputable providers include Linode and DigitalOcean (Note that Linode refers to their VPS’s as “Linodes” and DigitalOcean refers to theirs as “droplets”). As the process is more complicated than purchasing a domain name, I will go through setting up a VPS with Linode. However, the process should be mostly the same for other providers.

Setting up the VPS

Before creating an account with your VPS provider of choice, I would recommend scouring the internet for a promo code. Most providers will give you $20-$100 of credit to use on VPS’s for your first 1-2 months of usage. However, you should be careful not to create a monthly bill that you can’t pay after the 2 months expire. Once you have made your account, log in and go to the main dashboard.

Creating the VPS

Make sure you are on the “Linodes” tab, and click “Create a Linode”.

Choosing a Linux or BSD distribution

For this tutorial, I suggest that you use Ubuntu 20.04 LTS, which is a stable, relatively up-to-date, and easy-to-use Linux distribution. While you can choose to run your web server on a different distribution (Debian, CentOS, etc.), package installation will be different on distributions with a different package manager (Arch Linux, Gentoo, etc.) and configuration steps may also differ. Choose a distribution from the drop-down menu and continue.

Choosing a region

Now, choose the location which is physically closest to you. You can also use the Linode speedtest page if you are having trouble deciding between locations of similar distance.

Choosing a plan

Assuming that your website will not immediately serve a large amount of traffic, you should choose the smallest “Nanode” plan under the “Shared CPU” section, for $5 a month. The server plans are not fixed, so you can always upgrade if your website gains significant popularity.

Setting the root password

Next, you should set a root password. We will actually not be using this password to log into the server (we will use public key authentication instead), but Linode requires that you set one. I would suggest setting it to a long, random string of characters, as you will not need to remember it and can reset it at any time from the server dashboard.

Enable a private IP address

I suggest enabling a private IP address for your VPS. This will come in handy if you choose to additionally use this server to host a virtual private network (VPN), but should not be necessary for our website. Finally, after confirming the previous options and monthly price, click create.

Connecting the domain to the VPS

The VPS dashboard

Upon creating the VPS, open up its dashboard. If you are using Linode, you will be dropped right into it. You should see two numbers under IP addresses: the IPV4 address and the IPV6 address. The first address, which is an IPV4 address, will look something like xxx.xxx.xxx.xxx where each x is a decimal digit. The second, which is an IPV6 address, will look like xxxx:xxxx::xxxx:xxxx:xxxx:xxxx/64, where x is a hexadecimal digit (0-9 or a-f). Because humans are better at remembering words than 12 or 24-digit numbers, the domain name system (DNS) was created, allowing us to link these IP addresses with the domain that you purchased earlier. This is what is responsible for knowing which IPV4 address to go to when a user tries to visit a domain name.

With our domain registar, we will make two DNS records which tell the DNS server (hosted by your registrar by default) which IPV4 or IPV6 address to point to when someone visits your domain. I will walk through this process on Google Domains, but it should be similar for other providers.

The registrar dashboard

Sign into your domain registrar of choice. Click on the domain which you just purchased to view its options. In my case this is craftedwithcode.com.

DNS records

If using Google Domains, click on DNS records and then scroll down to the section labeled “Custom resource records”. We will be adding an A record with the IPV4 address and an AAAA record with the IPV6 address. All domain providers should have a section to add DNS records, it may just be named differently. For the A record fill in the fields as follows:

  • Name: leave blank
  • Type: A
  • TTL: 1m
  • Data: The IPV4 address from the VPS dashboard, in my case 66.228.38.123

Click “Add” and fill in the AAAA record as follows:

  • Name: leave blank
  • Type: AAAA
  • TTL: 1m
  • Data: The IPV6 address without the /64 from the VPS dashboard, in my case 2600:3c03::f03c:92ff:feca:4132

The name field refers to the subdomain to which the record applies, where a subdomain is the piece before the main domain name, i.e. subdomain in subdomain.name.tld. The type field determines which type of DNS record to set. In our case, we are setting A and AAAA records, which are used for mapping IPV4 and IPV6 addresses to domains respectively. The “TTL” field determine the time for the record change to go into effect (the time-to-live). Do not worry if your registrar does not allow a one minute time-to-live, you will just need to wait a bit longer for the record to go into effect. The data field varies depending on the record type but in our case it must be the appropriate type of IP address.

When finished, you should see both the A and AAAA records listed under your “Custom resource records”.

Congratulations, your domain name now points to the IP addresses of your VPS.

Subdomain redirection

On top of the common domain name format name.tld, domains are often qualified with subdomains as follows: subdomain.name.tld. The default (and most common) subdomain is www., and most websites are set up to have this subdomain redirect to the main domain. We will implement this redirection on our site for the sake of compatibility.

If using Google Domains, scroll up on the DNS records page to the “Synthetic records” section. Add a new subdomain forward with subdomain www and destination URL https://your-domain-here.tld. Choose “Permanent redirect”, “Forward path” and “Enable SSL”. Once you click add, you may see an error than no SSL certificate has been created for the domain yet. Don’t worry, as we will do this after setting up the web server.

On other providers, you should be able to supply the same information as a canonical name (CNAME) record. Providers should have documentation on the specifics of CNAME records on their platform.

Server configuration

Now that we have associated our VPS with our domain name and configured the DNS records, we can turn our attention to configuring the web server which will host the website.

Logging into the server

First, we need a secure way to log into the server. Remember before how we said that you don’t need to remember the root password? Well that is because using a password to log into a server openly visible to the internet is considered insecure. Given enough time and enough willingness to break into your server, anyone could brute force their way into root access to your website, an outcome we would really like to avoid.

However, we will still need to know the server’s root password to log in for the first time. Head back to your VPS dashboard and find the section where you can either view or reset the root password. If you find the root password, write it down and move on to the section on public key authentication. If not, reset it to something that you will remember and then continue.

Public key authentication

Lucky for us, these is a much more secure alternative called public key authentication. While I am not a cryptography expert, I’ll do my best to explain it. Public key authentication involves the creation of a key pair, consisting of a public key and a private key. In practical terms the keys are text files stored on your computer. You should treat your private key like your identity; anyone who has access to it can identify themselves as you (assuming that it is not encrypted). Your public key, on the other hand, can be used to securely give you access to a file, server, or other medium. Anyone with your public key can encrypt a file such that it can only be decrypted with your private key, simply by knowing your public key. We will apply this to securely log onto the web server using a protocol called SSH (the secure shell protocol).

Generating a keypair

Before we generate a keypair, we will discuss the competing algorithms which can be used for this purpose. RSA (standing for Rivest, Shamir, and Adleman, its inventors) is one of the most common cryptosystems, but it is currently thought that the ED25519 algorithm is superior in performance and security1. Thus, we will use ED25519 in this tutorial.

Now it is time to generate our keypair. Follow the section which corresponds to your operating system.

Linux, macOS, and BSD

Almost every Linux or BSD distribution comes with an implementation of SSH, usually OpenSSH, but it is possible that yours doesn’t. Check if ssh is installed by opening a terminal and running ssh -v, which will display the version of SSH if it is installed. If it’s not installed and you are on Debian or a Debian-based distribution such as Ubuntu or any of its variants, install ssh with sudo apt-get install openssh-client. On Arch-based systems the command is sudo pacman -S openssh and on Gentoo-based systems it is sudo emerge -av net-misc/openssh.

Once OpenSSH has been installed, we are ready to generate the keypair. Execute ssh-keygen -t ed25519. When asked where to save the key, press return to accept the default. I strongly recommend giving the key a passphrase (and thus encrypting it) by entering the desired password at the prompt. Now, continue to the section on copying over the public key.

Windows

Windows unfortunately is not in the UNIX family (unlike Linus, macOS, and BSD), so it requires some additional downloads to support our SSH keypairs. Fortunately, there is a great tool called PuTTY that will let us generate our keypair and log into the server afterwards. Download the 64-bit Windows installer for PuTTY here and install it.

Once PuTTY is installed, press the Windows key and type puttygen. This will open the keypair-generation tool with comes with PuTTY. Make sure to choose ED25519 as the “type of key to generate” and click “Generate”. Save the public key as id_ed25519.pub and the private key as id_ed25519. Make sure to give a passphrase when exporting the private key.

Installing a text editor

To edit our configuration files, we first need a text editor. Those who don’t know which editor to use should install GNU nano, with apt-get install nano on Debian/Ubuntu variants, pacman -S nano on Arch-based systems, or emerge -av app-editors/nano on Gentoo-based systems. Neovim is another great choice for those already familiar with vi or Vim. Once an editor is installed, continue to the next section.

Copying over the public key

Now that the keypair has been generated, we will copy the public key (not the private key) over to the server to allow ourselves to log in without the root password. On a UNIX-like system (Linux, macOS, BSD), run ssh-copy-id -i ~/.ssh/id_ed25519.pub root@domain.tld, assuming that you saved your public key in the default location. The -i flag allows us to provide an identity file, which in this case is an ED25519 public key. Of course, substitute domain.tld for your actual domain and ~/.ssh/id_ed25519.pub if you saved the public key elsewhere. The shell should prompt you for the root password which you wrote down earlier. Hit return, and if there aren’t any error messages the key was successfully copied over. Now continue to the section on removing the password login.

If you are on Windows, you will need to log onto the server with the password once to manually put in your public key for future authentication. Open PuTTY, giving the hostname root@domain.tld and the port 22. Select the SSH protocol and click “Open”. You should be prompted for the root password which you set upon creating the VPS. Most providers will allow you to copy the root password from the dashboard. Either copy and paste this into the PuTTY prompt or reset it if you do not have this option. Once logged into the server, open the ~/.ssh/authorized_keys file with your preferred text editor. If you don’t know what to use, run it with nano: nano -w ~/.ssh/authorized_keys. The authorized keys file is meant to contain the public keys of all computers which are authorized to login to the server. Open the public key file which you created earlier (id_ed25519.pub) on your Windows machine in Notepad. Copy the contents of the file with Ctrl-c and paste them into the authorized keys file in PuTTY by right-clicking. Save and exit your text editor (this is done in nano with Ctrl-x, then y, and finally Enter). Now, log out of the server and proceed to the next section.

Removing password login

Now that we have added public key authentication, we will prevent the server from allowing logins based on password alone. As long as you keep your private key safe, this effectively prevents anyone else from logging into the server, no matter how much time they put into cracking your password. If on Linux/macOS/BSD, log in at a terminal with ssh root@domain.tld. If on Windows, open PuTTY back up and fill in the fields as before. In both cases, you should be asked if you would like to trust the server which you are logging into. Click “Yes” if prompted or type yes in the terminal prompt. If all goes well, you should be logged in again.

Run nano -w /etc/ssh/sshd_config to open the SSH daemon (SSHD) config file. Press Ctrl-w to search, and type in UsePAM and press enter. Change the line to read UsePAM no. Do the same for PasswordAuthentication and change the line to read PasswordAuthentication no. Press Ctrl-x and then y, and finally Enter to save and quit nano. Finally run systemctl restart sshd to put these changes into effect, assuming that your server uses systemd (which is likely the case). If your server uses OpenRC (the case for Alpine Linux or Gentoo-based servers), instead run rc-service sshd restart.

Adding a non-root user

To further mitigate security concerns, it is best to completely disallow root logins to the server. When logged into the server, run the following command with your username: useradd -m -G wheel,users username. This creates a new user named username and puts them in the wheel group, allowing them to execute commands with root privileges. Next, we need to cofigure sudo such that users in the wheel group can execute commands as root. Run EDITOR=nano visudo and scroll down to the line which reads # %wheel ALL=(ALL) ALL. Uncomment this line by deleting the #, save, and quit.

To set up the new user, give them a password with passwd username. Enter your preferred password and hit return, and then enter it again. Note that characters will not appear as you type. Now, log in as the new user with su username and run sudo ls and enter your password. If this command runs with no errors, then we have a privileged non-root user.

To let us log into the server as our new user, we need to copy our authorized_keys file to the new user’s home directory and make sure that it has the proper permissions. While still logged in as the new user, run mkdir -p ~/.ssh to make the SSH directory in their home folder. Then, copy over the keys file with sudo cp /root/.ssh/authorized_keys ~/.ssh/. Now, restart nginx with sudo systemctl restart nginx. Log out of the new user with exit, and finally log out of the server with exit again. Log back in, but this time with ssh username@domain.tld. If the login works, we can remove root login completely.

Removing root login

Once logged into the server as your new user, run sudo nano -w /etc/ssh/sshd_config and find the line which has PermitRootLogin yes and change it to PermitRootLogin no. Save and quit the file and run sudo systemctl restart nginx. Log out of the server and try to log back in as the root user. If all was done correctly, you should be denied login. Log back in as your new user and continue to the next section.

Setting up the web server

Now that secure login is setup, we can start setting up the web server. We will be writing the website in a simple language called Markdown, which our site generator Hugo compiles it into static HTML and CSS. We will then use nginx (pronounced “engine x”, IPA: εndʒɪnεks) to serve these static pages to visitors of the site. Install nginx (along with a certificate tool) with your server’s package manager. For Ubuntu/Debian-based servers run apt-get install nginx python3-certbot-nginx, for Arch-based servers do pacman -S certbot certbot-nginx, and for Gentoo-based servers run emerge -av certbot certbot-nginx.

Configuring nginx

Now that nginx is installed, we will create a minimal working configuration file and use Certbot to generate an SSL certificate which allows for HTTPS connections. Open /usr/nginx/nginx.conf in your favorite editor (nano users will run nano -w /etc/nginx/nginx.conf) and delete the contents. To do this quickly in nano, go to the top of the file and hold Ctrl-k. In Vim, make sure you’re in normal mode with Escape, and then type ggdG. Next, paste in the following minimal configuration (usually with Shift-Ctrl-v on Linux terminal emulators of Right Click on PuTTY):

worker_processes  auto;

events {
        worker_connections  1024;
}

http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        keepalive_timeout  65;
        types_hash_max_size 4096;

        include /etc/nginx/sites-enabled/*;
}

Save the file and exit (Ctrl-x, y, and Enter in nano).

Site-specific configuration files

This configuration will allow you to serve multiple websites (with different domains) from the same server, each with their own configuration located in /etc/nginx/sites-enabled. We will begin by creating a site-specific configuration file in /etc/nginx/sites-available and symlinking it to /etc/nginx/sites-enabled. Change directories with cd /etc/nginx/sites-available and list the files there with ls. Open default with your text editor, and do the same procedure from before to delete the contents of the file and replace it by pasting in the following:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name domain.tld www.domain.tld;
    root /usr/share/nginx/html;
    location / {
        index index.html;
    }
}

Make sure to replace domain.tld and www.domain.tld with your domain name in the above before saving and exiting your editor. Now, all we need to do is start and enable nginx and then the nginx test page should be display when you visit your domain. Run systemctl enable nginx and then systemctl start nginx. Open up your website in your web browser and you should see a page with the message Welcome to nginx!. If you don’t see this message, don’t fret. Some TLDs, including .dev and .page force you to use SSL, so you won’t be able to load your website until after the next step.

Acquiring an SSL certificate

The wonderful people over at the EFF (an advocacy group for digital privacy) made a tool called certbot, which lets anyone easily get an SSL certificate for their website for free. To get your certificate, run certbot --nginx. Enter your email address when prompted (feel free to use a throwaway email) and then read over the terms and conditions here before typing a to agree and finally decide if you want to join the EFF mailing list. You will then be presented with a list with both your domain name domain.tld and the www subdomain www.domain.tld. Type the numbers which correspond to domain.tld and www.domain.tld and press return. When prompted, enter 2 to enable secure redirects. If all went, well there should be a message which says Congratulations! and tells you the location of your saved certificate. Open your web browser back up and visit http://domain.tld. If everything is correct, this should redirect to https://domain.tld.

Generating a static site with Hugo

At this point, you have a working web server which will serve any HTML files placed in /usr/share/nginx/html. If you are content with designing your website with raw HTML and CSS, you are able to do so and do not need the rest of this article. However, for most people, writing individual posts in Markdown will be easier and faster than HTML and CSS.

To convert our Markdown files into HTML and CSS, we need a static site generator. I chose Hugo, as it has many nice-looking themes from which to choose and is relatively simple to configure. Install Hugo with apt-get install hugo for Debian/Ubuntu, pacman -S hugo for Arch Linux, or emerge -av hugo for Gentoo.

Go to the nginx directory with cd /usr/share/nginx and make your Hugo site with hugo new site domain.tld, taking care to replace domain.tld with your domain. Now, head to the Hugo themes page and click on your favorite one. Click Download, which should take you to the GitHub page for that theme. Copy the URL of the git repository (looks like https://github.com/username/theme-name.git) and go back to the terminal. Change into your website directory with cd domain.tld and then clone the theme repository with git clone https://github.com/username/theme-name.git themes/themename. Make sure to substitute the URL in the above with the one you copied and themename with the name of your theme.

From this point on, the instructions vary significantly depending on which Hugo theme you chose, so I will just give some simple suggestions which assume you are using the terminal theme.

Linking Hugo and nginx

If using terminal theme, replace the contents of config.toml with the following:

baseurl = "/"
languageCode = "en-us"
theme = "terminal"
paginate = 5

[params]
  # dir name of your main content (default is `content/posts`).
  # the list of set content will show up on your index page (baseurl).
  contentTypeName = "posts"

  # ["orange", "blue", "red", "green", "pink"]
  themeColor = "orange"

  # if you set this to 0, only submenu trigger will be visible
  showMenuItems = 2

  # show selector to switch language
  showLanguageSelector = false

  # set theme to full screen width
  fullWidthTheme = false

  # center theme with default width
  centerTheme = false

  # set a custom favicon (default is a `themeColor` square)
  # favicon = "favicon.ico"

  # set post to show the last updated
  # If you use git, you can set `enableGitInfo` to `true` and then post will automatically get the last updated
  showLastUpdated = false
  # Provide a string as a prefix for the last update date. By default, it looks like this: 2020-xx-xx [Updated: 2020-xx-xx] :: Author
  # updatedDatePrefix = "Updated"

  # set all headings to their default size (depending on browser settings)
  # it's set to `true` by default
  # oneHeadingSize = false

[params.twitter]
  # set Twitter handles for Twitter cards
  # see https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started#card-and-content-attribution
  # do not include @
  creator = ""
  site = ""

[languages]
  [languages.en]
    languageName = "English"
    title = "Terminal"
    subtitle = "A simple, retro theme for Hugo"
    owner = ""
    keywords = ""
    copyright = ""
    menuMore = "Show more"
    readMore = "Read more"
    readOtherPosts = "Read other posts"
    missingContentMessage = "Page not found..."
    missingBackButtonLabel = "Back to home page"

    [languages.en.params.logo]
      logoText = "Terminal"
      logoHomeLink = "/"

    [languages.en.menu]
      [[languages.en.menu.main]]
        identifier = "about"
        name = "About"
        url = "/about"
      [[languages.en.menu.main]]
        identifier = "showcase"
        name = "Showcase"
        url = "/showcase"

Now, run hugo -D to generate the default pages for your theme. Next, we will edit the nginx configuration file to point to our Hugo directory. Open /etc/nginx/sites-available/default in your text editor and change the root /usr/share/nginx/html; to root /usr/share/nginx/domain.tld/public;. Exit the editor and restart nginx with systemctl restart nginx and you should be able to see the default theme homepage by visiting your domain.

Customizing your theme

Now that the website is up and running, you can feel free to customize /usr/share/nginx/domain.tld/config.toml file, which allows you to change theme color, default language, and many other settings.

Making posts

Once you are satisfied with your customized theme, you can make a new post with hugo new posts/post-name-here.md and edit the post file with nano -w content/posts/post-name-here.md. Upon opening the file, you will see several lines enclosed between a pair of +++. These lines contain several variables which you can set for each post, including title, author, date, etc. After the closing +++, you can begin to write your post using standard Markdown (here’s a good introduction to Markdown if you’ve never used it before). Once you are done writing a post, simply save and exit, and generate the post with hugo -D.

Future workflow

At any time in the future, if you want to add to your website, it only takes a few steps. First, log into your server with ssh username@domain.tld. Then, change to your website directory with cd /usr/share/nginx/domain.tld. Make a new post with hugo new posts/new-post-name.md. Upon the post in your editor with nano -w content/posts/new-post-name.md. Save and quit when you are done and run hugo -D to convert it to HTML. Congratulations, you now have a secure, fast-loading, nice-looking website with posts only written in Markdown.

Doing more

Stay tuned for future articles on multi-site web servers, custom Linux distributions, and more. If this article helped you, feel free to share it with your friends or take a look at my GitLab or GitHub.

Credits

This article was inspired by a number of existing tutorials and other resources, including but not limited to:

References

Sources which I relied on for specific technical points are listed here:

  1. This article on Gitlab about SSH keypairs and encryption algorithms