I’ve been wanting to start this blog for quite some time. As I learn and delve deeper into systems and programming, it is helpful to document and reflect on my experiences in a way that both helps me visualize my growth and help others learn from my projects.
There is no better candidate for a first post than going over the process of setting up this very blog: a simple, statically generated site using Hugo as a framework, managed and built automatically using webhooks to a remote VPS, all proxied via NGINX. You can check out all the code on my Github, it’s quite a bit simpler than I’d envisioned it would be.
The Web Server
Since the site is only a minimal, statically generated site, I chose to go with an Ubuntu VPS. Setup is fairly easy—after installing the image, update, configure the local user, and install the following packages:
Additionally, we set up an A record on our domain endpoint (I am using the root) that points to the VPS IP address so users can reach the web server. You may also want to configure a CNAME record for the ‘www.’ subdomain. After configuring and enabling UFW to our desired level, we need to allow traffic for HTTP and HTTPS on ports 80 and 443 with sudo ufw allow ‘Nginx Full’. We also need to set up the root directory from which our html files will be stored and set our permissions so hugo can write to it:
sudo mkdir -P /var/www/blog/public
sudo chown -R $USER:www-data /var/www/blog/public
sudo chmod -R 755 /var/www/blog/public
Once our base directories for the web server have been created, we can now configure NGINX to serve the files generated by Hugo via https:
-
Enable and start the NGINX server:
sudo systemctl enable --now nginx -
Create the nginx config for the root directory, using temporary values for certbot to enable ssl. Create
/etc/nginx/sites-available/blogand add the following contents, substituting your domain:server { listen 80; server_name domain.com www.domain.com; return 301 https://$host$request_uri; } server { listen 443 ssl; http2 on; server_name domain.com www.domain.com; # Temporary self-signed placeholders (Certbot will replace these) ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; root /var/www/blog/public; index index.html; location / { try_files $uri $uri/ =404; } # Deny access to hidden files location ~ /\. { deny all; } # Enable caching for static assets location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2?|ttf|svg)$ { expires 30d; access_log off; } # Proxy GitHub webhook requests to local listener location /hugo-webhook { proxy_pass http://127.0.0.1:9000/hooks/hugo-deploy; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Optional: only allow POST limit_except POST { deny all; } } } -
Enable the site by linking it to the sites enabled-directory, then test and reload the server:
sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx -
Generate and enroll SSL certificates with certbot, this will set up autorenewal:
sudo certbot --nginx -d domain.com
You can now test visiting your site, since no files are being served you should get a 404 response.
The (Hugo) Blog
Now that the webserver is successfully up and running, it is time to install and deploy Hugo, configuring it to auto-build HTML files in the project root. We start by installing Hugo (I built from source as the versions in some distro repos are quite outdated):
curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest \
| grep browser_download_url \
| grep linux-amd64.tar.gz \
| grep extended \
| cut -d '"' -f 4 \
| xargs wget -q -O /tmp/hugo.tar.gz && \
tar -xzf /tmp/hugo.tar.gz -C /tmp && \
sudo mv /tmp/hugo /usr/local/bin/hugo && \
rm /tmp/hugo.tar.gz
Start a new terminal session or source your bashrc to update your $PATH. Now we will create the main project directory from which our site will be built. I placed mine under my home directory at /home/$USER/hugo-site:
cd
hugo new site hugo-site
cd hugo-site/
Set up the basic configuration for your Hugo site by editing hugo-site/hugo.toml. This is where we set the directory in which we want to build our files:
…
publishDir = "/var/www/blog/public"
…
Assuming your Github SSH Keys have already been configured, we will now initialize and sync the repo. You will want to create a bare repo from the Github webui to sync with first:
git init -b main
git add .
git commit -m "Initial commit"
git remote add origin REMOTE-URL
git push origin main
Now that our repo is set up we can configure our webhook to automatically rebuild and deploy the site to the public folder whenever we push updates to the git repo. We start by creating a deployment script which will be called when the webhook receives a POST request:
-
Create the file
/usr/local/bin/deploy-hugo.shand add the following contents:#!/bin/bash # Navigate to the Hugo site cd ~/hugo-site || exit # Pull latest changes git fetch origin git reset --hard origin/main git clean -fd # Build the site HUGO_LOG=~/hugo-site/hugo.log hugo --cleanDestinationDir -
Make the file executable:
sudo chmod +x /usr/local/bin/deploy-hugo.sh
Next we install, configure, and enable the webhook server to run in the background, listening for requests:
-
Install the server:
sudo apt install webhook -
Build the hook for requests from github. Create the file
~/hooks/hugo.jsonand add the following contents:[ { "id": "hugo-deploy", "execute-command": "/usr/local/bin/deploy-hugo.sh", "command-working-directory": "/home/ubuntu/hugo-site", "response-message": "Deploying Hugo site...", "secret": "UNIQUE SECRET TOKEN HERE" } ] -
Now start the web server temporarily to test that it is receiving HTTP requests:
webhook -hooks /home/ubuntu/hooks/hugo.json -port 9000 -verbose -
We can now configure webhooks for the git repo from Github’s webui. You will need to set the following values:
- Payload URL:
https://domain.com/hugo-webhook - Content type:
application/json - Secret:
UNIQUE SECRET TOKEN (from hugo.json) - Enable
SSL Verification - Trigger on
Just the push event - Set to
Active
Update the webhook and check to ensure Github is receiving an HTTP 200 response.
-
If the server is responding correctly we can now create a systemd service to automatically run the webhook server in the background. Create the file
/etc/systemd/system/hugo-webhook.serviceand add the following contents:[Unit] Description=GitHub Webhook Listener for Hugo After=network.target [Service] User=ubuntu WorkingDirectory=/home/ubuntu ExecStart=/usr/bin/webhook -hooks /home/ubuntu/hooks/hugo.json -port 9000 Restart=always RestartSec=5 StandardOutput=syslog StandardError=syslog SyslogIdentifier=webhook [Install] WantedBy=multi-user.target -
Enable the service:
sudo systemctl daemon-reload sudo systemctl enable --now hugo-webhook
Now that the service is running we can test by creating a new post with hugo new posts/test.md, adding, commiting, and pushing afterward. Hugo should be triggered by the webhook, automatically building and deploying a static page to the public web directory.
If you are seeing this site that means it has all worked correctly. To update my blog I simply need to clone my repo to a local client, write a new post as a simple markdown file, and push the updated files. This will trigger the webhook which runs the script updating the cloned repo on the webserver and then running hugo to build and serve the static html files.
I’m excited to explore this further and share here as I continue to build out my server and automations! Next up, configuring a GUI console with Guacamole.
Useful Links: Markdown Cheat Sheet | GitHub Repo