Why?#
There are countless content management systems that make building and deploying a website easy, often with minimal effort. However, most of them are bloated with features I’ll never use. I wanted something simple and lightweight. Just a personal website I could host on my own VPS.
Tools Used#
Hugo: The star of the show. Hugo is a fast and lightweight static site generator written in Go. It uses Markdown files to build the entire site, making it simple to create posts.
Docker: I prefer to use containerized services whenever possible. Here, Docker is used to run our web server in a clean and reproducible environment.
Caddy: An open-source web server known for its simplicity and power. Caddy automatically fetches and renews SSL certificates, making HTTPS setup effortless. We use Caddy inside a Docker container to serve our static website and act as a reverse proxy.
GitHub Actions: Manually building and copying site files quickly became tedious. So, I leveraged GitHub to manage my source code and set up a CI/CD pipeline using GitHub Actions. Now, every time I push to the
main
branch, the site is automatically built and deployed.
Let’s get started#
Build a Hugo Website#
We are going to build a very basic website for this demonstration. Run the below commands to create a Hugo site with the Blowfish theme.
hugo new site quickstart
cd quickstart
git init
git submodule add -b main https://github.com/nunocoracao/blowfish.git themes/blowfish
mkdir -p config/_default
rm hugo.toml
cp themes/blowfish/config/_default/*.toml config/_default/
sed -i '/^\s*#\s*theme\s*=\s*"blowfish"/s/^#\s*//' config/_default/hugo.toml
hugo server
Now open your browser and navigate to localhost:1313 to see your newly created Hugo Site.

Add Content#
Let’s add a new page to the website.
hugo new content content/posts/example.md
This will create a markdown file in the content/posts
directory. Replace the contents of the file with the below lines:
---
title: "Example Post Title"
date: 2025-07-22T21:19:29Z
draft: false
---
Lorem Ipsum
Save the file and start the Hugo server to view your progress.
Read the Blowfish documentation to understand the configuration files and to customise the website.
For example,
baseURL = 'https://example.org/'
title = 'Example Site Title'
When you are satisfied with the content, run the following command to create the static site, which will generate all the necessary files and will place it in the public
directory.
hugo
Deploy Caddy with Docker#
For this setup, I assume you’re working on a VPS with Docker installed, a public IP address, and a domain name pointing to that IP.
- If Docker isn’t installed yet, follow the official Docker installation guide to install Docker Engine on the VPS or the machine where you are hosting the website from.
- Most cloud providers offer free trials or always-free tiers that are great for hosting a small personal site.
- For the domain, use a reputable registrar (I use Porkbun) and create an A record that points to the VPS’s public IP.
Login to the VPS (I’m running Debian on my VPS) and execute the below commands.
mkdir caddy
cd caddy
nano docker-compose.yml
Paste the following to the docker-compose.yml
file and press ctrl + x
to save the file and exit.
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./site:/srv
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
Once done create the Caddyfile
nano Caddyfile
and paste the below
example.com {
root * /srv
file_server
handle_errors {
@404 {
expression {http.error.status_code} == 404
}
rewrite @404 /404.html
file_server
}
}
The Caddyfile contains the configuration for the web server and here we are instructing it to serve the static files from the srv
directory. Note that the srv
directory in the Docker container is bind-mounted to the site
directory in our local filesystem. So we need to put our static site files to the site
folder.
Start the container by typing
docker compose up -d
Verify that the container is running by typing
docker ps
GitOps with Github Actions#
I assume here that you are familiar with git
and that you have created the repository and configured necessary variables as well.
In order to create a continuous deployment pipeline we are going to create a workflow
. To do that create the below file in your root Hugo site directory.
.github/workflows/example_flow_name.yml
Replace the contents of the file with the following.
name: Deploy Hugo site to VPS
on:
# Runs on pushes targeting the default branch
push:
branches:
- main
# Default to bash
defaults:
run:
shell: bash
jobs:
# Build job
build:
runs-on: ubuntu-latest
env:
DART_SASS_VERSION: 1.89.2
HUGO_VERSION: 0.148.0
HUGO_ENVIRONMENT: production
TZ: Europe/Dublin
steps:
- name: Install Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb
sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Install Dart Sass
run: |
wget -O ${{ runner.temp }}/dart-sass.tar.gz https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz
tar -xf ${{ runner.temp }}/dart-sass.tar.gz --directory ${{ runner.temp }}
mv ${{ runner.temp }}/dart-sass/ /usr/local/bin
echo "/usr/local/bin/dart-sass" >> $GITHUB_PATH
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install Node.js dependencies
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Build with Hugo
run: |
hugo \
--gc \
--minify
# Deployment job - working config
- name: Deploy to server via rsync
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
source: ./public/*
target: /home/user/caddy/site
strip_components: 1
The next time you push code to your main branch, this workflow will be triggered. It will use a GitHub Runner
to build the Hugo site and the static site files are then copied over to the VPS using SCP.
You can visit your repository and take a look at the Actions
tab to see the progress of the workflow.

If everything went correctly, you can visit example.com and your website will be visible.
Conclusion#
If you followed along, by this time you should have a simple website which is hosted publicly. You can configure the website as you please later on.
I hope this guide helped you. Please feel free to share your feedback.