James Desmond

How this site was made

hugo self-hosting devops

This site was created with a few main goals in mind:

  • Being easy to update from any device which I own
  • Cheap to host, without reliance on ‘free’ services
  • Stable, with ease of updating and upgrading
  • Storing my writing in a domain which I own and control
  • Allowing me to learn more about linux server management through practice
  • Easy, simple backups

Some sites which inspired the design choices here are:

  • danluu.com
    • A great blog full of interesting technical writing, usually containing thorough investigation and research
  • stallman.org
    • An interesting point of view from a much more interesting person, along with a wonderfully simple template
  • sheldonbrown.com
    • Bicycle website with in depth analysis of all kinds of bicycle technology
  • Hacker News
    • Forum for Silicon valley types, great online discussion here on technical subjects with a simple and clean UI

Based on my goals and inspiration, I found two possible paths.

  1. Using Hugo, a simple framework for generating static websites
  2. Using Netlify, a complete product for generating and maintaining static websites, with a very permissive ‘free’ tier.

I chose to utilize Hugo, instead of a free option like Netlify, Github pages, or other free solutions. I desired to have more control over my content than I would have with those services. I also felt that I would be missing out on an opportunity to learn more about linux server administration if I went with a pre-made service. I did end up using a few cloud services, like Github and Digital Ocean but I am working on plans to replace them with self-hosted options.

Negatives of going the self-hosted route are:

  • Lack of a security team, using a VPS or my own server requires me to be in charge of hardening
  • Increased administration workload compared to premade options

Positives of self-hosting are:

  • Less reliance on centralized services
  • I pay for usage, and not for access to support or additional features
  • I learn and demonstrate subject area knowledge all in one project

How to create your own similar site using a VPS, Hugo, Nginx, and Github

Architecture overview

The setup works like this: Hugo generates static HTML from Markdown files. GitHub Actions builds the site in CI and deploys it to a VPS via rsync. Nginx serves the static files. There is no Hugo, git, or build process on the server — it just serves files.

Local machine: write markdown → git push
    ↓
GitHub Actions: hugo --minify → rsync to VPS
    ↓
VPS (nginx): serves static HTML from /home/james/www/

Required before starting

  • A domain you control. I used namecheap.
  • A server. I chose to use a VPS, and went with Digital Ocean with a small droplet running Ubuntu.
    • You could use your own server if you own a physical machine. If you don’t have a static IP address, use a DDNS service like noip (however this is an additional external dependency)
  • A GitHub account

Configuring the VPS

The first step is to create an account with a VPS provider. The goal is to get an IP address and a root password to a Linux machine. Once you have these things, record them, and attempt to use an ssh client to connect and login to the root account.

Note on these instructions

If a command is preceded by #, it is to be run as the root user. If it is preceded by $ it is to be run as the non-root user.

Once signed in as root, do the following security tasks:

  1. Use adduser command to create a new non-root, non-sudoers account on the machine. e.g. adduser james

  2. Run # apt update -y && apt upgrade -y && apt install nginx

    • This will update, upgrade, and install nginx. Hugo is not needed on the server — it builds in CI.
  3. Now use your favorite text editor to edit and then save /etc/ssh/sshd_config The lines being changed are:

    • PermitRootLogin should be set to no, this will disable ssh login as root
    • PasswordAuthentication should be set to no — only key-based auth
    • Port should be changed from 22 to any number of your choosing except for 80 or 443, which nginx will use for HTTP or HTTPS. Record what port you change it to so you know how to connect to the VPS later
  4. # service sshd restart

  5. Log in as your newly created non-root user, on your newly chosen port.

Configuring DNS

Before proceeding, go to your DNS provider and create an A record to point your domain name to the IP address given by your VPS provider. Namecheap instructions for A records.

Setting up Hugo locally

Hugo runs on your local machine (and in CI), not on the server. Install it on your development machine:

  1. Install Hugo on your local machine. See Hugo’s install docs — on macOS you can use brew install hugo.

  2. $ hugo new site site_name where site_name is what you want to call your site folder and repository.

  3. $ cd site_name to enter the directory

  4. $ hugo new posts/hello-world.md to create a new Markdown file (Hugo uses Goldmark)

  5. Edit content/posts/hello-world.md — set draft to false in the front matter, and add some words below the header. Save and exit.

  6. $ hugo server -D to preview locally at localhost:1313. You should see your post. Press Ctrl+C when done.

Setting up the theme

You have two options for themes:

Option A: Use a theme submodule — Find a theme from Hugo Themes and add it with git submodule add <theme-url> themes/<theme-name>, then set theme = '<theme-name>' in config.toml.

Option B: Local layouts (what I do) — Instead of relying on an external theme, create your own layout files directly in the layouts/ directory. This gives you full control and removes the external dependency. You need at minimum:

  • layouts/_default/baseof.html — base template
  • layouts/_default/list.html — list pages
  • layouts/_default/single.html — individual posts
  • layouts/partials/header.html and footer.html

I started with a theme and eventually overrode every template locally, then removed the theme submodule entirely.

Configuring nginx

  1. As root on the VPS: # cd /etc/nginx/sites-available

  2. # rm default then # touch site_name to create an empty file with your blog’s chosen name

  3. Using your editor of choice, use this nginx conf file:

	server {
    		listen 80 default_server;
    		listen [::]:80 default_server;
    		root /home/james/www;
    		index index.html;
    		server_name jamesdesmond.org www.jamesdesmond.org;
    		location / {
        		try_files $uri $uri/ =404;
    		}
	}

Make sure to edit the root value and server_name value.

  1. # ln -s /etc/nginx/sites-available/site_name /etc/nginx/sites-enabled/site_name to create a symbolic link for nginx

  2. # rm /etc/nginx/sites-enabled/default to clean up

  3. # service nginx restart to reload nginx and have it read your newly created configuration file

  4. Set up TLS with Let’s Encrypt: # apt install certbot python3-certbot-nginx then # certbot --nginx -d yourdomain.org -d www.yourdomain.org

If your DNS records have propagated (takes somewhere between an hour and a day) you should be able to visit your domain name through a web browser and see your page. Here are some basic troubleshooting steps if you do not see it:

  • In command prompt of any computer, execute ping www.jamesdesmond.org, replacing my domain with yours. If you see your IP address resolved through the domain name, then DNS has propagated. If not, wait and try pinging in a little bit.
  • Check your nginx logs: # tail -f /var/log/nginx/error.log

Setting up GitHub Actions CI/CD

The deployment pipeline builds Hugo in GitHub Actions and deploys the static output to the VPS via rsync. No Hugo or git is needed on the server.

  1. $ git init in your Hugo root directory

  2. Create a .gitignore with public/ — there is no need to track Hugo’s output.

  3. $ git add -A then $ git commit -m "initial commit"

  4. Create a new repo on github.com (without README or gitignore).

  5. $ git remote add origin git@github.com:yourname/site_name.git

  6. $ git push -u origin master

  7. Create .github/workflows/main.yml:

name: CI

on:
  push:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: '0.158.0'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy via rsync
        uses: burnett01/rsync-deployments@7.0.1
        with:
          switches: -avz --delete
          path: public/
          remote_path: /home/james/www/
          remote_host: ${{ secrets.SERVER_IP }}
          remote_port: ${{ secrets.PORT }}
          remote_user: ${{ secrets.SERVER_USERNAME }}
          remote_key: ${{ secrets.SSH_KEY }}

This workflow checks out the code, builds with a pinned Hugo version, and rsyncs the output to the VPS. rsync updates files in place with --delete to remove stale files, so there is no downtime during deploys. If the Hugo build fails, nothing gets deployed.

  1. On your VPS, generate an SSH key for the deploy user: $ ssh-keygen -t ed25519. Add the public key to ~/.ssh/authorized_keys on the VPS.

  2. Go to your GitHub repo Settings → Secrets and variables → Actions. Add these secrets:

  • SERVER_IP — your VPS IP address
  • SERVER_USERNAME — your non-root user (e.g. james)
  • SSH_KEY — the private key contents
  • PORT — your custom SSH port
  1. $ git add -A && git commit -m "Add CI/CD pipeline" && git push

Now, on GitHub, in your repo, on the Actions tab you should see this new commit. You can watch the deployment process, and read any errors that may occur.

That is it! Now the pipeline from GitHub to the VPS to the web is complete. To test: edit your hello-world.md file, commit and push. Then visit your website from a web browser — you should see the change once the CI process completes (usually under 30 seconds).

My next post goes over how I create new content for the site from client devices.

If you see any issues or have suggestions for this guide please contact me at james@jamesdesmond.org and I appreciate you taking the time to visit my site!