How To Deploy & Self-Host Your Own Blog w/ NGINX.

Austin Newton
16 min readJan 7, 2024

--

Prerequisites:

  • Internet Connection.
  • The ability to purchase a domain.
  • Fundamental knowledge of Linux and computer networking.

Intro

If you have been considering creating your own personal website, like a personal blog or a documentation site, this is the guide for you. In this guide, I will be creating an IT documentation website. A place for you to save and share your projects and learning.

We will be using NGINX as our web server, and we will be using a theme from Jekyll for our website. This will be hosted locally on Ubuntu Server 22.04. Once everything is set up and configured we will set up our reverse proxies, TLS encryption, and firewall rules. Finally, we will go purchase a domain and set up port forwarding.

Installing Ubuntu 22.04

The first step we need to accomplish is to install our operating system, Ubuntu. If you are installing Ubuntu on a virtual machine or bare metal, the process will be identical. The link to the Ubuntu 22.04 ISO is here:

https://ubuntu.com/download/server

Once we have completed the installation process, we need to install a GUI for Ubuntu Server. This step is optional but makes things much more convenient when we get into customizing our site. Note: This will increase the RAM requirement of your webserver.

Installing GUI for Ubuntu Server (🤦‍♂️)

To do this, we need to install Tasksel. Enter the following commands:

sudo apt-get install tasksel
sudo tasksel

You can choose which desktop environment. The default choice is the Debian / GNOME environment. Once it completes, the server will need a reboot for the GUI to appear.

Installing NGINX

Now that we have Ubuntu up and running with a GUI, we can install our webserver, NGINX. To do this, open up a bash terminal and enter the following:

Install the prerequisites:

sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring

Import an official NGINX signing key so apt could verify the packages authenticity. Fetch the key:

curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

Verify that the downloaded file contains the proper key:

gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg

The output should contain the full fingerprint 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 as follows:

pub   rsa2048 2011-08-19 [SC] [expires: 2024-06-14]
573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
uid nginx signing key <signing-key@nginx.com>

If the fingerprint is different, remove the file.

To set up the apt repository for stable NGINX packages, run the following command:

echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list

If you would like to use mainline NGINX packages, run the following command instead:

echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list

Set up repository pinning to prefer our packages over distribution-provided ones:

echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
| sudo tee /etc/apt/preferences.d/99nginx

To install NGINX, run the following commands:

sudo apt update
sudo apt install nginx

Installing Jekyll

Next we need to install Jekyll.

sudo apt update
sudo apt install ruby-full build-essential zlib1g-dev git
echo '# Install Ruby Gems to ~/gems' >> ~/.bashrc
echo 'export GEM_HOME="$HOME/gems"' >> ~/.bashrc
echo 'export PATH="$HOME/gems/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Note: This makes sure that we don’t install some of the dependencies as our root user.

gem install jekyll bundler

Installing Chirpy Theme

Next, we need to install our open-source theme, Chirpy. This step will require you to have a GitHub account so if you don’t have one, now is the time to create one.

Now that you have a GitHub account navigate to the Chirpy starter page here:

Then select the green button at the top right, and select ‘Create a new repository’. Here you can choose the repository name, and if it is public or private.

Now that our Chirpy repository is created, we need to pull those files down from GitHub to our local web server. In our bash terminal, we will enter the following:

git clone git@<YourGitHubUser>/<YourChirpyRepoName>.git

We now have our website template on our local webserver.

Installing VS Code

Since we will be dealing with configuration, markdown, and HTML files we should install a 3rd party IDE. This will save ourselves the headache of dealing with vi/vim. This is ultimately the reason why we installed our GUI.

sudo snap install --classic code

Once VS code has been installed, you can open it from any directory with:

code .

Customizing Your New Site

Now that we have all the necessary prerequisites installed, we can serve our website locally, and start to customize it. To do this, we need to first serve our website locally.

bundle exec jekyll s

This command will serve out the template website on your machine’s IP, port 4000. Now if you open a web browser and navigate to your loopback or machine IP, you will see your template.

To start customizing your new site, we need to navigate into the repository we pulled down from GitHub on your local machine and then enter the VS code command:

code .

Your IDE will open up, and you will be greeted by a nice file explorer on the left. These files and folders are the contents of your website. Before we begin to create any posts, we should check out a few other files first.

Note: If you do not see the changes you make on the locally served website, turn off the web server and rerun the following command:

bundle exec jekyll s

Config.yml / Contact.yml / Share.yml

In the config.yml file, you can change things like the main title and description for your website, social media links, Google Analytics, comments, and more.

In the contact.yml file, you can enter your social media links and choose which social media icons are present on your website.

In the share.yml file, you can allow readers to share your post on other websites. In this file you can choose which social media sites are available to share too(Ex: GitHub, Twitter, etc.).

Your First Post

Creating posts for your new website is very simple, but takes getting used to. To start, we will navigate to the _posts directory in vscode and create a new markdown file (.md). The name of the file needs to be formatted in a specific way. Here is an example:

2024-01-03-My-First-post.md

The file name needs to include the date you want the post to show it was posted on, in year-month-day format. If you have posted blogs on other sites and want to move the posts over, you can post the original post date here. Or if you are creating a new post, you would put today’s date. The name of the post is arbitrary, just make sure you have the date in the correct format.

Now that our markdown file is created, we need to add a specific header that is required on all posts to work correctly. Here is an example header:

---
title: My first post!
date: 2024-01-03 12:00
categories: [webserver, NGINX]
tags: [linux, ubuntu, bash, technology] #all lowercase
image: /assets/puppy.jpg
---

As you can see it lists the title and date for the post, what categories you want to put this post in, SEO tags, and an image path.

The image path is refers to the thumbnail picture that will be displayed on the home screen of the website.

Once our header is complete we can start working on the content of the post. Creating your content using the markdown syntax can be a little clunky at first but you will get used to it quickly. Here is a cheat sheet for the syntax.

Creating a Sidebar Tab

If you would like to add another tab to your main sidebar on the left, it is a similar process to creating a new post. Navigate to the _tabs directory in vscode and create a new markdown file. Note: It is not required to put the date in the file name for tabs. Once we have the file created we need to enter a header. Here is an example of a ‘contact’ section header.

---
layout: page # This is the default
title: contact
icon: fas fa-envelope
order: 5
---

This example created a new tab named contact, it has an envelope icon from Font Awesome and it will be the 5th button down on the sidebar. Use this as a template for whatever tab you want to create.

Email Contact

Codeblock output

If you created the contact tab above, and you would like to give users who visit the site the ability to contact you by email, we can do this with Getform. Create an account with Getform (there is a free tier), and input your unique URL into the top of the code block below.

<form accept-charset="UTF-8" action="YOUR-URL-HERE" method="POST" enctype="multipart/form-data" target="_blank">
<input type="email" name="email" placeholder="Your Email">
<input type="text" name="name" placeholder="Your Name">
<textarea name="message" placeholder="Your Message"></textarea.
<input type="hidden" name="_gotcha" style="display:none !important">
<button type="submit">Send</button>
</form>
<link rel="stylesheet" href="/yourpath/yourfile.css"

Next, we need to create the CSS file that will style our form into the one in the picture above. In the vscode editor, create a new CSS file in your main repo folder. The one that contains all of your other website folders. Copy the code block below into it, and then change the path to reflect accurately.

input[type=message], input[type=email], input[type=text], textarea {
width: 100%; /* Full width */
padding: 12px; /* Some padding */
border: 1px solid #ccc; /* Gray border */
border-radius: 4px; /* Rounded borders */
box-sizing: border-box; /* Make sure that padding and width stays in place */
margin-top: 6px; /* Add a top margin */
margin-bottom: 16px; /* Bottom margin */
resize: vertical /* Allow the user to vertically resize the textarea (not horizontally) */
}

/* Style the submit button with a specific background color etc */
button[type=submit] {
background-color: #484a4a;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}

/* When moving the mouse over the submit button, add a darker blue color */
button[type=submit]:hover {
background-color: #21618c;
}


/* Hide Streamlit Branding */
#MainMenu {visibility: hidden;}
#footer {visibility: hidden;}
#header {visibility: hidden;}

Production

Once you are done with customizing your website and creating your first round of posts, we need to set up our website for production. First, we need to shut down the temporary web server we are using to see our changes locally. This can be done by pressing Ctrl + C in the terminal you originally ran the

bundle exec jekyll s

command on. Once that's complete we need to enter this command in our repo directory:

JEKYLL_ENV=production bundle exec jekyll b

This will create a new directory in our repo directory called _site. This directory is our complete website, with all the changes you just made included. Jekyll converted all those markdown files into HTML and CSS files just like that. Now we need a web server, and we can point our root page to our HTML files inside.

Webserver & Reverse Proxy

Now that we have our website created and ready to go, we need to configure NGINX as our webserver to serve those files and pictures when someone requests them, and we will create a reverse proxy with NGINX as well. This will make it so that user requests come to the proxy instead of our web server itself. So the users will be interacting with the web server by proxy.

To start there are two important directories we want to have open just to make things more convenient:

  • /etc/nginx
  • /usr/share/nginx/html

The first directory is where our NGINX configuration file is stored, and the second is where we will move our _site folder.

First let’s open up vscode in the /etc/nginx directory. Then open up the nginx.conf file. As the name suggests, this is the configuration file for the NGINX webserver. To make things easy, I have created a template for you to base your configuration on.

http
{
include /etc/nginx/mime.types;

server
{
listen 80 default_server;
listen [::]:80 default_server;

server_name you.website.name;



location /
{
proxy_pass http://127.0.0.1:3000;
}
}


server
{
listen 3000;
root /usr/share/nginx/html/_site;
}

}

events
{



}

This code block does a lot. We will break it down:

include /etc/nginx/mime.types;

This snippet includes another file in the /etc/nginx directory. This file specifies all the types of files our NGINX file will recognize and serve out. If you open up the mime.types file in vscode, you can see all that is included.

server
{
listen 80 default_server;
listen [::]:80 default_server;

server_name you.website.name;

This snippet creates a new server that is listening on port 80 for both ipv4 and ipv6 connections. Next, we declare the name of our server, if you already own a domain, and have a subdomain picked out, you would enter it here.

location / 
{
proxy_pass http://127.0.0.1:3000;
}
}

This is the second half of the first server block in our configuration template. This snippet is saying, “If this server receives a request on port 80, what should it do?” and we have set it up to forward or pass the connection to our loopback address, this time over port 3000.

server
{
listen 3000;
root /usr/share/nginx/html/_site;
}

}

events
{



}

Our final snippet of the config template shows another server code block. This code block says, that if this new server receives a request on port 3000 serve up this particular file. And that file is set to our _site folder from earlier.

If you haven’t put it all together already, this entire code block does the following:

If the proxy server receives a request on port 80,
we will pass the traffic back to our loopback on
port 3000. The next server will receive the request
on port 3000 and serve the content requested to the
end user.

Note: This is not intended to be a one-size-fits-all configuration file. While this may work for your setup by coincidence, most will need to adjust the file to their environment.

Now that our configuration file is set up (for now at least). We need to move our _site folder. You can move it wherever you would like, but I will move mine to /usr/share/nginx/html.

sudo mv _site /usr/share/nginx/html

You can confirm the _site directory was moved to the correct spot by doing a quick ls in the HTML directory.

Now that our NGINX server is partly configured we can turn on the webserver by running the following command in the /etc/nginx directory.

sudo nginx

To confirm the NGINX process is running you can run the following:

sudo systemctl status nginx

To test, we can navigate to the web server’s private IP address in our web browser. If you used another port other than port 80, you may need to specify it like so:

192.168.1.25:8080

If you do not see your website in your web browser check the following:

  • Confirm you are entering the correct private IP / Port combination.
  • Confirm your NGINX web server is running.
  • Confirm your webserver is listening on the port specified. (netstat -ltupn)

Host-Based Firewall

The main reason we are using a reverse proxy with NGINX is so that end users aren’t interacting with our web server directly. The traffic is being forced through the proxy to go to our web server. All of this reverse proxy work would be for nothing if we still allowed connections directly to our web server right? To make sure this isn’t the case, we can use host based firewall rules with UFW. If you don’t know what UFW is, it stands for Uncomplicated Firewall. This comes with Ubuntu by default, and it allows us to set up host-based firewall rules with quick and simple syntax. To start, we need to turn on UFW.

sudo ufw enable
sudo ufw status

Now that we know UFW is up and running, we can begin to create rules. Here are the ones we need for our website:

We want to ALLOW traffic on port 80 from any IP.

sudo ufw allow 80

Next we want to allow connections from 127.0.0.1 (Local host / Loop back) on port 5000.

sudo ufw allow from 127.0.0.1 to 127.0.0.1 port 5000

Finally, we want to make sure that our host-based firewalls default action is to deny or block incoming connections. This will ensure ONLY the connections we have allowed with our specific firewall rules are entering our server. We can check this by running the following command:

sudo ufw status verbose

If you run the command and your firewalls incoming default action isnt deny, you can run this command:

sudo ufw default deny incoming

Port Forwarding

Now that we have our host-based firewall configured, we need to set up our router for incoming, unwarranted inbound traffic. Usually, we want to block all unwarranted that wants to come into our private network. But since we want to expose our website to the internet, we need to make an exception to the rule. We can achieve this by navigating to our port forwarding section in our router. We only need to set up one rule for now. In the example above, I have set up a port forwarding rule to accept incoming connections on port 80 and send them to my web server’s private IP address. This router has an option to automatically update the firewall according to the port forwarding rule you input. With other brands, you may need to do that manually.

DNS Configuration

Now that we have tested our site within our private network, configured our host-based firewall, and set up port forwarding we can configure our domain DNS settings. To do this we need to navigate to wherever you purchased your domain, assuming they are still managing DNS. Next, we need to find the section where we can add DNS records, and add an A record. You can choose whatever you like for the subdomain, but popular options are blog.example.com or www.example.com. Then you need to match that with your router’s WAN side IP. You can do this by following the link:

https://whatismyipaddress.com/

Once you have created your A record, it could take a few minutes to 24 hours for the name server to update. Keep checking back by entering your new website name to check it is working correctly.

Note: If you are not able to reach your webserver after the name server updates, check the following:

  • Confirm port forwarding rules are correct / Add firewall rules manually if needed.
  • Confirm that host-based firewall rules are correct.
  • Confirm your A record is correct.

Cloudflare

Technically, the last few steps going forward are not mandatory but highly encouraged. With the use of Cloudflare, we can utilize their proxy, but also their encryption.

With Cloudflare, the user requests will be sent through Cloudflare’s proxy, and then to your network. Additionally, we can also add TLS encryption from the end user to our NGINX proxy. To start we need to create an account with Cloudflare and move our DNS records from our domain providers nameservers to Cloudflare’s. Here is a link to the migration documentation.

https://developers.cloudflare.com/registrar/get-started/transfer-domain-to-cloudflare/

Next, we need to acquire a certificate for our web server. We can do this easily by going to SSL/TLS > Origin Server > Create Certificate in Cloudflare. Choose which encryption and which hostname(s) you would like the certificate to cover. Once the keys are created, copy and paste the Origin cert into a notepad and save it as whatever name you want, but it needs to be a .pem file.

Next do the same for the private key, but instead of .pem file, save it as a .key file.

Finally, move both of the files to the following directory:

/etc/ssl/certs

Once the keys are in place, we need to do some more configuration to our proxy and router.

Enabling HTTPS

As of now our proxy and port forwarding rules only allow for unencrypted HTTP traffic. We need to adjust the configurations to allow for HTTPS.

First, navigate to your nginx.conf file in vscode, and add the following 5 lines:

worker_processes auto; #NEW 

http
{
include /etc/nginx/mime.types;

server
{
listen 80 default_server;
listen 443 ssl default_server; #NEW
listen [::]:80 default_server;
listen [::]:443 default_server; #NEW
ssl_certificate /etc/ssl/certs/your-origin-cert.pem #NEW
ssl_certificate_key /etc/ssl/certs/your-private-key.key #NEW
server_name you.website.name;



location /
{
proxy_pass http://127.0.0.1:3000;
}
}


server
{
listen 3000;
root /usr/share/nginx/html/_site;
}

}

events
{



}

Next, we need to add a port forwarding rule for port 443 connections to be sent to our proxy.

Finally, we will open port 443 on our host based firewall.

sudo ufw allow 443

Our proxy is now ready to receive HTTPS traffic.

Cloudflare Encryption mode

If you navigate to SSL/TLS > Overview, you can choose which encryption mode to use. Since we have already done the leg work of adding an origin certificate to our proxy, we can set the mode to strict.

Now, if we navigate to our website name in a web browser we shouldn't have to ‘proceed anyway’ to access the website. We should see the Lock in our search bar as well.

Cloudflare IPs

Since all of our traffic is now being routed through Cloudflare's proxies, we can use that for an often-overlooked advantage. Cloudflare posts a list of all of the IP address ranges they use, here:

Since we know all of these IP ranges, we can allow these ranges into our host-based firewall, and BLOCK all other connections. Forcing all connections to our proxy to go through Cloudflare.

sudo ufw allow CLOUDFLARE-IP-HERE proto tcp to any port 443
sudo ufw allow CLOUDFLARE-IP-HERE proto tcp to any port 80

End

This concludes the guide to your self-hosting your own blog. This is by far the longest, and most complicated guide I have ever done. Hopefully you found this helpful! Remember to check the ‘Helpful Resources & Documentation” section for any questions you might have.

Helpful Resources & Documentation

--

--

Austin Newton

IT Professional with a passion for networking and systems.