How this site was made
hugo self-hosting devopsThis 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.
- Using Hugo, a simple framework for generating static websites
- 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:
Use
addusercommand to create a new non-root, non-sudoers account on the machine. e.g.adduser jamesRun
# 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.
Now use your favorite text editor to edit and then save
/etc/ssh/sshd_configThe lines being changed are:PermitRootLoginshould be set tono, this will disable ssh login as rootPasswordAuthenticationshould be set tono— only key-based authPortshould 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
# service sshd restartLog 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:
Install Hugo on your local machine. See Hugo’s install docs — on macOS you can use
brew install hugo.$ hugo new site site_namewheresite_nameis what you want to call your site folder and repository.$ cd site_nameto enter the directory$ hugo new posts/hello-world.mdto create a new Markdown file (Hugo uses Goldmark)Edit
content/posts/hello-world.md— setdrafttofalsein the front matter, and add some words below the header. Save and exit.$ hugo server -Dto preview locally atlocalhost: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 templatelayouts/_default/list.html— list pageslayouts/_default/single.html— individual postslayouts/partials/header.htmlandfooter.html
I started with a theme and eventually overrode every template locally, then removed the theme submodule entirely.
Configuring nginx
As root on the VPS:
# cd /etc/nginx/sites-available# rm defaultthen# touch site_nameto create an empty file with your blog’s chosen nameUsing 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.
# ln -s /etc/nginx/sites-available/site_name /etc/nginx/sites-enabled/site_nameto create a symbolic link for nginx# rm /etc/nginx/sites-enabled/defaultto clean up# service nginx restartto reload nginx and have it read your newly created configuration fileSet up TLS with Let’s Encrypt:
# apt install certbot python3-certbot-nginxthen# 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.
$ git initin your Hugo root directoryCreate a
.gitignorewithpublic/— there is no need to track Hugo’s output.$ git add -Athen$ git commit -m "initial commit"Create a new repo on github.com (without README or gitignore).
$ git remote add origin git@github.com:yourname/site_name.git$ git push -u origin masterCreate
.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.
On your VPS, generate an SSH key for the deploy user:
$ ssh-keygen -t ed25519. Add the public key to~/.ssh/authorized_keyson the VPS.Go to your GitHub repo Settings → Secrets and variables → Actions. Add these secrets:
SERVER_IP— your VPS IP addressSERVER_USERNAME— your non-root user (e.g.james)SSH_KEY— the private key contentsPORT— your custom SSH port
$ 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!