I’m a software engineer by trade. I’ll write about programming topics, such as data structures, algorithms, programming tutorials, and career tips. I’m a huge fan of AWS and have 2 certifications so far (I’m going for all 10!).

Find me on , and .

Hosting Your Website For Free With GitHub Pages and Hugo

13 minute read

I can’t believe I didn’t discover this secret until recently. Apparently it’s possible to host a website entirely for free even with a custom domain! The only thing you’ll need to pay for is your custom domain name.

Firstly, I’ll give a brief overview of how websites work. Then, I’ll provide a step-by-step tutorial on how to achieve free hosting. Skip down to The Actual Tutorial if you already have some background on how websites work.

A Refresher On How The Browser Serves Websites

(Did you get that bad pun? Refresh your browser if you didn’t.)

When you type in a domain name into your web browser, the web browser does a bunch of things under-the-hood to fulfill your request. The domain name is resolved into an IP address via DNS, the IP address eventually leads us to a server, the browser verifies the certificates sent over by the server, and finally the server gives us a file which the browser renders into the page that we see.

If you’re curious on all the juicy details, go into your browser tools (inspect), refresh your page, and watch all the magic happen:

It’s Technically One File, But…

Nobody writes their entire website in one file anymore. Each line you see in the GIF above is an additional file or request.

But we have to start somewhere.

The first file we get back from the server will be an index.html file, and this index.html file will contain a bunch of references to other files, such as other HTML, CSS, JS files, and image files too. The browser will continue processing each of these files it got back, taking note of any additional files that it needs and fetching those too. It will continue this process until every line of code has been rendered and there are no more additional files to fetch. All the files that the browser fetched are “static files” which the browser needs in order to render the page.

Every time you type a website into your browser, all these things happen under the hood to deliver us that final page that we see. Think of it like ordering from a restaurant: you order one dish, but there’s a bunch of things happening behind the scenes (the chefs cooking in the kitchen, the grocery shopping that happened beforehand, etc.) before your dish is served to you.

The browser doesn’t care how such “static files” are prepared for us - it just cares that it gets the “static files”. There are two types of preparation: dynamic sites and static sites.

What’s a Dynamic Site?

Content Management Systems (CMS) like Wordpress, Drupal, Joomla, or Ghost all dynamically generate the site before returning the HTML/CSS/JS files to the web browser. They do it at runtime, which means that when they receive a browser request, they will generate the HTML/CSS/JS files on the spot and then return it to the browser. Here’s a diagram of how a dynamic site works:

Think of dynamic sites as lazy - they will procrastinate until the very last moment. Step 2 is where the server is scrambling to generate the HTML/CSS/JS files that it doesn’t yet have.

Such server-side rendering or dynamic sites is how the old web is run. Before the use of Javascript was widespread, the easiest way to provide users with dynamic functionality in the browser was for the server to render different HTML/CSS back to the user. Now, Javascript is so powerful that it can single-handedly power apps on the web (single page applications). Which leads us to…

What’s a Static Site?

Static sites will give back the very same HTML/CSS/JS files every time. It doesn’t change - it’s static. Here’s a diagram of how a static site works:

Notice that there’s no need for the server to generate the HTML/CSS/JS files on the spot. That’s because the files have already been prepared. Think of static sites as proactive - they prepared ahead of time and so they don’t need to scramble.

Dynamic Sites vs Static Sites

Here are some downsides to dynamic sites:

  • Dynamic sites are slower than static sites because the server needs to do extra work to generate the HTML/CSS/JS files before it gives it back to the browser.
  • Dynamic sites require more processing power because they need to do extra work to generate the HTML/CSS/JS files before giving it back to the browser.
  • Dynamic sites typically run on a content-management system and have logins and databases, which are susceptible to being hacked.

In contrast, static sites don’t need to worry about slowness or extra power because the HTML/CSS/JS files stay the same. They are static. There’s no logins and no databases, and therefore there’s nothing to hack.

By now, I hope you’re convinced that static-sites are the way to go. So, how do we create a static site?

Static-Site Generators!

Okay, you can technically go and code your raw HTML/CSS/JS code from scratch. But that would be horribly inefficient, not to mention error-prone. Enter static-site generators! You get all the benefits of a CMS along with the speed and security.

Static-site generators generate all the contents required for a static site. This includes all the HTML/CSS/JS files and any assets like images.

The static-site generator works similarly to a dynamic-site except that the static-site generator will generate all the files offline, and we can take those files and upload them to a web server. This is in contrast to dynamic sites, which will generate these files on the spot.

This tutorial uses hugo as our static-site generator, but there are plenty of other options too, like jekyll.

The Actual Tutorial

Now that you have a solid understanding of how websites work, we can start creating one! This tutorial uses macOS in the examples, but the instructions should be similar for other OSes. We will use the tool hugo as our static-site generator and GitHub Pages to host our pages for free.

Creating a Skeleton Website With Hugo

We will start by installing hugo and quickly setting up a skeleton static website. You can refer here for the official Hugo tutorial. I’ve listed the steps below for convenience:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# install hugo
brew install hugo

# create new site
hugo new site mysite

# add a theme
cd mysite
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
echo theme = \"ananke\" >> config.toml

# add a post
hugo new posts/my-first-post.md

# start the hugo server
hugo server -D

Navigate your browser to http://localhost:1313 and you should see your skeleton site.

If you don’t like this theme, simply browse the library of themes and find one you like to use. I also encourage you to check out the hugo documentation to make further improvements to your site.

GitHub Pages Setup

Now it’s time to setup our GitHub Pages. You can always refer to the official GitHub Pages documentation, but it’s a bit dense and requires you to put the pieces together. That’s why I wrote this guide!

0. Some Background

There are 3 types of GitHub Pages sites: user, organization, and project.

Pages Type Repository Name URL
User <username>.github.io http(s)://<username>.github.io
Organization <organization>.github.io http(s)://<organization>.github.io
Project <repository> http(s)://<username>.github.io/<repository> or http(s)://<organization>.github.io/<repository>

Each user or organization can only have one main site, but each user or organization can have an unlimited number of project sites. However, each project site is really just a sub-page of your user or organization’s main site. Therefore, we want to create a main site.

In my case, I wanted to host multiple sites with different domains, so I got around this by creating multiple organizations that are owned by my account. If you only need one site, then you can just stick with your user’s Pages type.

Additionally, for the GitHub free tier, if we want to use GitHub Pages, we must set the repository that contains the generated static site as public. I didn’t want to expose my pre-processed files, so I will be creating a private repo for my pre-processed files and a public repo for my post-processed files.

1. Create a GitHub Organization (Optional)

Again, this step is only for those who want to host multiple sites with GitHub Pages. On the top right next to your profile icon, click the dropdown and click “Your organizations”.

Then click “New organization”, and choose “Create a free organization”. You should be greeted with this screen:

Fill in the fields as appropriate and beat the captcha.

2. Create a Private Repository for the Hugo Source Files

Now we need to create a private repository in GitHub and link it to the local git repository we created in the hugo section. We will be committing to this repo every time we update our website.

On the GitHub home page at the top left, click on “New”:

You should be greeted with this screen:

Fill in the fields as appropriate, and follow the instructions to link your local repo to your remote repo.

3. Create a Public Repository for the Generated Static Files

Now we need to create a public repository in GitHub that contains the post-processed files. This is where the static files to our website will live. We won’t be committing to this repo - instead, later we will setup a GitHub Action to do this on our behalf everytime we commit a change to the private repo.

If you’re pursuing the path of using your user account, simply create a new repo. If you’re pursuing the path of using an organizational account, navigate to the organization first and then create a new repo. Either way, you should be greeted with the following:

For the “Repository name” part, you must use the format <username>.github.io or <organization>.github.io. This is critical for GitHub Pages to work! (I’ve already done this step, which is why it’s showing red for me.)

4. Give Permissions for Private Repo to Commit to Public Repo

Next, we’ll need to generate a private-public key-pair that will act as a deploy key. Full instructions here, but I’m reproducing here for convenience:

1
2
3
4
ssh-keygen -t rsa -b 4096 -C "$(git config user.email)" -f gh-pages -N ""
# You will get 2 files:
#   gh-pages.pub (public key)
#   gh-pages     (private key)

The public key goes to the private repo. The private key goes to the public repo.

Re-read that just in case you didn’t get it. It’s easy to make a mistake here especially because both steps look almost the same, so be extra careful. I’ll provide all screenshots just to make sure you’ll get it right.

Let’s start with adding the private key to the private repo:

  1. Navigate to your private repo, and click on “Settings”.
  2. Click on “Secrets”.
  3. Click on “New repository secret”.
  4. Finally, add your private key.

Let’s then add the public key to the public repo:

  1. Navigate to your public repo, and click on “Settings.”
  2. Click on “Deploy keys”.
  3. Click on “Add deploy key”.
  4. Finally, add your public key, and be sure to check the box “Allow write access”.

After the next step, this will allow our private repo to make commits to the public repo. This allows us to keep our pre-processed files private.

5. Setup GitHub Action

Now it’s time to setup some automation. We want it such that every time we commit a change to our private repo, the website will automatically be updated. We can leverage GitHub Actions to help us here.

To add a new GitHub Action to your private repo, simply add the following file to your private repo:

.github/workflows/github-pages.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: github pages

on:
  push:
    branches:
      - main  # Set a branch to deploy
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.88.0'
          extended: true
      
      - name: Clean
        run: rm -rf ./public

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
          external_repository: username/username.github.io
          publish_branch: main
          publish_dir: ./public

Fill in your appropriate information for external_repository and hugo-version.

You can find the hugo version with:

1
hugo version

Finally, when you’re ready, commit and push this file:

1
2
3
git add .
git commit -m "adding github action to automatically update website"
git push origin main

6. Verify Website Works!

After pushing this file, you should be able to navigate to https://<username>.github.io or https://<organization>.github.io and see if your site is live!

If nothing shows up, navigate to your private repo’s “Actions” page to diagnose the issue:

If something went wrong, you’ll see that either there are no workflows or that the workflows ended up red.

If it works, then congratulations! You now have a free site in the form of username.github.io or organization.github.io.

This is optional and only for those who care about having their own domain name. (I mean, why wouldn’t you? Domain names can be as cheap as $12/year, so you might as well have it customized.)

1. Buy a Domain Name

Any domain registrar will do. I personally use Google domains. Once you decide on a domain name you like, go ahead and purchase it. Don’t worry about purchasing any extra stuff. You own the rights to the domain annually and you’ll need to renew every year if you want to keep it.

2. Add a CNAME Record

In your domain registrar console, add a CNAME Record to point to username.github.io or organization.github.io. If you’re not using a subdomain (the stuff that comes before example.com), you need to use www.example.com. Do NOT have example.com directly point to username.github.io or organization.github.io. For full details, see the GitHub Pages documentation on custom domains.

In my case, I used a subdomain.

3. Tell GitHub Pages About Your Domain

Go to your public repo, click on “Settings”, then “Pages”. You should see the following:

Add in your custom domain and click “Save”.

Then, move over to your private repo and modify the following file: .github/workflows/github-pages.yml. Add a line at the very bottom with your custom domain name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: github pages

on:
  push:
    branches:
      - main  # Set a branch to deploy
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.88.0'
          extended: true
      
      - name: Clean
        run: rm -rf ./public

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
          external_repository: username/username.github.io
          publish_branch: main
          publish_dir: ./public
          cname: website.com # this is the new line!

When you’re ready, commit and push this file:

1
2
3
git add .
git commit -m "deploy to custom domain name"
git push origin main

Enter your domain name into your browser. Right now, it should either be empty, errored, or pointed to your old website if you had one. For now, we’ll need to wait. Keep checking back at your domain name until it starts to display the same site as username.github.io. The propagating of the DNS CNAME record can take up to 48 hours, though it should be much shorter. It took about 2 hours for me.

4. Enable HTTPS

Next, we need to enable HTTPS. Go to your public repo, click on “Settings”, then “Pages”. You should see the following:

Click on “Enforce HTTPS”.

Now you’ll need to wait for GitHub Pages to issue the HTTPS certificate. I’ve heard that this can take up to 12 hours, but I only needed to wait less than 1 hour for this.

Conclusion

Congratulations! Now you have a freely hosted website using GitHub Pages along with a static-site generator hugo. Furthermore, you have a nicely automated pipeline setup such that every time you commit a change to your private repo, a GitHub Action will automatically update your website. Super cool!